Updated JSON events response to use PyAMS_skin helpers
authorThierry Florac <thierry.florac@onf.fr>
Wed, 11 Apr 2018 16:46:31 +0200
changeset 527 5dd1aa8bedd9
parent 526 b15153f45957
child 528 15f595c8c4b0
child 647 e8ebdd1417ea
Updated JSON events response to use PyAMS_skin helpers Added prefix to edit forms Updated tables attributes
src/pyams_content/component/association/zmi/__init__.py
src/pyams_content/component/association/zmi/paragraph.py
src/pyams_content/component/extfile/zmi/__init__.py
src/pyams_content/component/gallery/zmi/__init__.py
src/pyams_content/component/gallery/zmi/file.py
src/pyams_content/component/gallery/zmi/paragraph.py
src/pyams_content/component/illustration/zmi/__init__.py
src/pyams_content/component/illustration/zmi/paragraph.py
src/pyams_content/component/links/zmi/__init__.py
src/pyams_content/component/paragraph/interfaces/keynumber.py
src/pyams_content/component/paragraph/keynumber.py
src/pyams_content/component/paragraph/zmi/__init__.py
src/pyams_content/component/paragraph/zmi/contact.py
src/pyams_content/component/paragraph/zmi/container.py
src/pyams_content/component/paragraph/zmi/frame.py
src/pyams_content/component/paragraph/zmi/header.py
src/pyams_content/component/paragraph/zmi/html.py
src/pyams_content/component/paragraph/zmi/keynumber.py
src/pyams_content/component/paragraph/zmi/keypoint.py
src/pyams_content/component/paragraph/zmi/milestone.py
src/pyams_content/component/paragraph/zmi/pictogram.py
src/pyams_content/component/paragraph/zmi/templates/milestones.pt
src/pyams_content/component/paragraph/zmi/templates/paragraphs.pt
src/pyams_content/component/paragraph/zmi/templates/pictograms.pt
src/pyams_content/component/paragraph/zmi/verbatim.py
src/pyams_content/component/paragraph/zmi/video.py
src/pyams_content/component/theme/zmi/manager.py
src/pyams_content/component/theme/zmi/portlet.py
src/pyams_content/component/video/zmi/paragraph.py
src/pyams_content/features/alert/__init__.py
src/pyams_content/features/alert/zmi/__init__.py
src/pyams_content/features/alert/zmi/container.py
src/pyams_content/features/renderer/zmi/__init__.py
src/pyams_content/features/review/zmi/__init__.py
src/pyams_content/locales/fr/LC_MESSAGES/pyams_content.mo
src/pyams_content/locales/fr/LC_MESSAGES/pyams_content.po
src/pyams_content/locales/pyams_content.pot
src/pyams_content/profile/zmi/__init__.py
src/pyams_content/reference/pictograms/zmi/__init__.py
src/pyams_content/reference/zmi/__init__.py
src/pyams_content/reference/zmi/table.py
src/pyams_content/root/__init__.py
src/pyams_content/root/zmi/search.py
src/pyams_content/root/zmi/sites.py
src/pyams_content/shared/blog/zmi/manager.py
src/pyams_content/shared/common/zmi/__init__.py
src/pyams_content/shared/common/zmi/dashboard.py
src/pyams_content/shared/common/zmi/i18n.py
src/pyams_content/shared/common/zmi/properties.py
src/pyams_content/shared/common/zmi/search.py
src/pyams_content/shared/common/zmi/security.py
src/pyams_content/shared/common/zmi/summary.py
src/pyams_content/shared/common/zmi/types.py
src/pyams_content/shared/common/zmi/workflow.py
src/pyams_content/shared/form/zmi/field.py
src/pyams_content/shared/form/zmi/properties.py
src/pyams_content/shared/imagemap/zmi/area.py
src/pyams_content/shared/imagemap/zmi/container.py
src/pyams_content/shared/imagemap/zmi/paragraph.py
src/pyams_content/shared/imagemap/zmi/properties.py
src/pyams_content/shared/logo/zmi/paragraph.py
src/pyams_content/shared/logo/zmi/properties.py
src/pyams_content/shared/site/__init__.py
src/pyams_content/shared/site/link.py
src/pyams_content/shared/site/manager.py
src/pyams_content/shared/site/zmi/container.py
src/pyams_content/shared/site/zmi/link.py
src/pyams_content/shared/view/zmi/properties.py
src/pyams_content/shared/view/zmi/reference.py
src/pyams_content/skin/resources/img/internal-link.png
src/pyams_content/skin/resources/js/pyams_content.js
src/pyams_content/skin/resources/js/pyams_content.min.js
src/pyams_content/skin/resources/js/tinymce/onflinks/plugin.js
src/pyams_content/skin/resources/js/tinymce/onflinks/plugin.min.js
--- a/src/pyams_content/component/association/zmi/__init__.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/component/association/zmi/__init__.py	Wed Apr 11 16:46:31 2018 +0200
@@ -22,7 +22,7 @@
 from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
 from pyams_form.interfaces.form import IInnerSubForm
 from pyams_skin.layer import IPyAMSLayer
-from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION, MANAGE_PERMISSION
+from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
 from pyams_zmi.interfaces.menu import IPropertiesMenu
 from z3c.table.interfaces import IValues, IColumn
 
@@ -32,7 +32,8 @@
 from pyams_pagelet.pagelet import pagelet_config, Pagelet
 from pyams_skin.table import BaseTable, SorterColumn, NameColumn, ImageColumn, I18nColumn, TrashColumn, \
     VisibilitySwitcherColumn
-from pyams_skin.container import switch_element_visibility
+from pyams_skin.container import switch_element_visibility, delete_container_element
+from pyams_skin.event import get_json_switched_table_refresh_event, get_json_table_row_refresh_event
 from pyams_skin.viewlet.menu import MenuItem
 from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter
 from pyams_utils.traversing import get_parent
@@ -58,19 +59,12 @@
     """Association item add form, JSON renderer"""
 
     def get_ajax_output(self, changes):
-        table = AssociationsTable(self.context, self.request)
-        table.update()
         return {
             'status': 'success',
             'message': self.request.localizer.translate(_("Association was correctly added.")),
-            'events': [{
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'MyAMS.skin.refreshSwitchedTable',
-                    'object_id': table.id,
-                    'table': table.render()
-                }
-            }]
+            'events': [
+                get_json_switched_table_refresh_event(self.context, self.request, AssociationsTable)
+            ]
         }
 
 
@@ -79,19 +73,12 @@
 
     def get_associations_table(self):
         target = get_parent(self.context, IAssociationTarget)
-        table = AssociationsTable(target, self.request)
-        table.update()
         return {
             'status': 'success',
             'message': self.request.localizer.translate(self.successMessage),
-            'events': [{
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'MyAMS.skin.refreshSwitchedTable',
-                    'object_id': table.id,
-                    'table': table.render()
-                }
-            }]
+            'events': [
+                get_json_table_row_refresh_event(target, self.request, AssociationsTable, self.context)
+            ]
         }
 
 
@@ -112,9 +99,7 @@
 class AssociationsTable(ProtectedFormObjectMixin, BaseTable):
     """Associations view inner table"""
 
-    @property
-    def id(self):
-        return 'associations_{0}_list'.format(self.context.__name__)
+    prefix = 'associations'
 
     hide_header = True
     sortOn = None
@@ -135,11 +120,8 @@
             'data-ams-location': absolute_url(IAssociationContainer(self.context), self.request),
             'data-ams-tablednd-drag-handle': 'td.sorter',
             'data-ams-tablednd-drop-target': 'set-associations-order.json',
-            'data-ams-visibility-target': 'switch-association-visibility.json'
+            'data-ams-visibility-switcher': 'switch-association-visibility.json'
         }
-        attributes.setdefault('tr', {}).update({
-            'data-ams-delete-target': 'delete-association.json'
-        })
         return attributes
 
     @reify
@@ -250,49 +232,22 @@
     """Associations table trash column"""
 
 
-@view_config(name='delete-association.json', context=IAssociationContainer, request_type=IPyAMSLayer,
-             permission=MANAGE_PERMISSION, renderer='json', xhr=True)
+@view_config(name='delete-element.json', context=IAssociationContainer, request_type=IPyAMSLayer,
+             renderer='json', xhr=True)
 def delete_association(request):
     """Delete association"""
-
-    from pyams_content.component.paragraph.zmi.container \
-        import ParagraphContainerTable, ParagraphTitleToolbarViewletManager
-
-    translate = request.localizer.translate
-    name = request.params.get('object_name')
-    if not name:
-        return {
-            'status': 'message',
-            'messagebox': {
-                'status': 'error',
-                'content': translate(_("No provided object_name argument!"))
-            }
-        }
-    if name not in request.context:
-        return {
-            'status': 'message',
-            'messagebox': {
-                'status': 'error',
-                'content': translate(_("Given association name doesn't exist!"))
-            }
-        }
-    del request.context[name]
-    parent = get_parent(request.context, IAssociationTarget)
-    table = ParagraphContainerTable(parent, request)
-    viewlet = ParagraphTitleToolbarViewletManager(parent, request, table)
-    viewlet.update()
-    return {
-        'status': 'success',
-        'handle_json': True,
-        'events': [{
-            'event': 'myams.refresh',
-            'options': {
-                'handler': 'PyAMS_content.paragraphs.updateToolbar',
-                'object_name': parent.__name__,
-                'toolbar_tag': viewlet.render()
-            }
-        }]
-    }
+    output = delete_container_element(request)
+    if output.get('status') == 'success':
+        from pyams_content.component.paragraph.zmi import get_json_paragraph_toolbar_refresh_event
+        parent = get_parent(request.context, IAssociationTarget)
+        output.update({
+            'handle_json': True,
+            'events': [
+                get_json_paragraph_toolbar_refresh_event(parent, request),
+                get_json_switched_table_refresh_event(parent, request, AssociationsTable)
+            ]
+        })
+    return output
 
 
 @pagelet_config(name='associations.html', context=IAssociationTarget, layer=IPyAMSLayer,
--- a/src/pyams_content/component/association/zmi/paragraph.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/component/association/zmi/paragraph.py	Wed Apr 11 16:46:31 2018 +0200
@@ -36,6 +36,7 @@
 from pyams_content.features.renderer.zmi import BaseRenderedContentRenderer
 from pyams_content.features.renderer.zmi.widget import RendererFieldWidget
 from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.event import get_json_widget_refresh_event
 from pyams_utils.adapter import adapter_config
 from pyams_utils.traversing import get_parent
 from pyams_viewlet.viewlet import viewlet_config
@@ -88,6 +89,8 @@
 class AssociationParagraphPropertiesEditForm(BaseParagraphPropertiesEditForm):
     """Association paragraph properties edit form"""
 
+    prefix = 'association_properties.'
+
     @property
     def title(self):
         content = get_parent(self.context, IWfSharedContent)
@@ -134,20 +137,9 @@
         output = super(AssociationParagraphInnerAJAXEditForm, self).get_ajax_output(changes)
         updated = changes.get(IAssociationParagraph, ())
         if 'renderer' in updated:
-            form = AssociationParagraphInnerEditForm(self.context, self.request)
-            form.update()
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'MyAMS.skin.refreshWidget',
-                    'parent_id': '{0}_{1}_{2}'.format(
-                        self.context.__class__.__name__,
-                        getattr(form.getContent(), '__name__', 'noname').replace('++', ''),
-                        form.id),
-                    'widget_name': form.widgets['renderer'].name,
-                    'content': form.widgets['renderer'].render()
-                }
-            })
+            output.setdefault('events', []).append(get_json_widget_refresh_event(self.context, self.request,
+                                                                                 AssociationParagraphInnerEditForm,
+                                                                                 'renderer'))
         return output
 
 
--- a/src/pyams_content/component/extfile/zmi/__init__.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/component/extfile/zmi/__init__.py	Wed Apr 11 16:46:31 2018 +0200
@@ -29,6 +29,7 @@
 # import packages
 from pyams_content.component.association.zmi import AssociationItemAJAXAddForm, AssociationItemAJAXEditForm
 from pyams_content.component.extfile import EXTERNAL_FILES_FACTORIES
+from pyams_content.component.paragraph.zmi import get_json_paragraph_markers_refresh_event
 from pyams_content.component.paragraph.zmi.container import ParagraphContainerCounterBase
 from pyams_form.security import ProtectedFormObjectMixin
 from pyams_pagelet.pagelet import pagelet_config
@@ -72,6 +73,8 @@
     icon_class = 'fa fa-fw fa-file-text-o'
     icon_hint = _("External files")
 
+    marker_type = 'extfiles'
+
     @property
     def count(self):
         return len([file for file in IAssociationContainer(self.context).values()
@@ -124,17 +127,8 @@
     def get_ajax_output(self, changes):
         output = super(ExtFileAJAXAddForm, self).get_ajax_output(changes)
         if output:
-            counter = ExtFilesCounter(self.context, self.request, self, None)
-            counter.update()
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'PyAMS_content.paragraphs.updateMarkers',
-                    'object_name': self.context.__name__,
-                    'marker_type': 'extfiles',
-                    'marker_tag': counter.render()
-                }
-            })
+            output.setdefault('events', []).append(get_json_paragraph_markers_refresh_event(self.context, self.request,
+                                                                                            self, ExtFilesCounter))
         return output
 
 
@@ -142,6 +136,8 @@
 class ExtFilePropertiesEditForm(AdminDialogEditForm):
     """External file properties edit form"""
 
+    prefix = 'extfile_properties.'
+
     legend = _("Update file properties")
     icon_css_class = 'fa fa-fw fa-file-text-o'
     dialog_class = 'modal-large'
@@ -184,6 +180,8 @@
     icon_class = 'fa fa-fw fa-file-image-o'
     icon_hint = _("Images")
 
+    marker_type = 'extimages'
+
     @property
     def count(self):
         return len([file for file in IAssociationContainer(self.context).values()
@@ -235,17 +233,8 @@
     def get_ajax_output(self, changes):
         output = super(ExtImageAJAXAddForm, self).get_ajax_output(changes)
         if output:
-            counter = ExtImagesCounter(self.context, self.request, self, None)
-            counter.update()
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'PyAMS_content.paragraphs.updateMarkers',
-                    'object_name': self.context.__name__,
-                    'marker_type': 'extimages',
-                    'marker_tag': counter.render()
-                }
-            })
+            output.setdefault('events', []).append(get_json_paragraph_markers_refresh_event(self.context, self.request,
+                                                                                            self, ExtImagesCounter))
         return output
 
 
@@ -295,6 +284,8 @@
     icon_class = 'fa fa-fw fa-file-video-o'
     icon_hint = _("Videos")
 
+    marker_type = 'extvideos'
+
     @property
     def count(self):
         return len([file for file in IAssociationContainer(self.context).values()
@@ -338,17 +329,8 @@
     def get_ajax_output(self, changes):
         output = super(ExtVideoAJAXAddForm, self).get_ajax_output(changes)
         if output:
-            counter = ExtVideosCounter(self.context, self.request, self, None)
-            counter.update()
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'PyAMS_content.paragraphs.updateMarkers',
-                    'object_name': self.context.__name__,
-                    'marker_type': 'extvideos',
-                    'marker_tag': counter.render()
-                }
-            })
+            output.setdefault('events', []).append(get_json_paragraph_markers_refresh_event(self.context, self.request,
+                                                                                            self, ExtVideosCounter))
         return output
 
 
@@ -390,6 +372,8 @@
     icon_class = 'fa fa-fw fa-file-audio-o'
     icon_hint = _("Audios files")
 
+    marker_type = 'extaudios'
+
     @property
     def count(self):
         return len([file for file in IAssociationContainer(self.context).values()
@@ -433,17 +417,8 @@
     def get_ajax_output(self, changes):
         output = super(ExtAudioAJAXAddForm, self).get_ajax_output(changes)
         if output:
-            counter = ExtAudiosCounter(self.context, self.request, self, None)
-            counter.update()
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'PyAMS_content.paragraphs.updateMarkers',
-                    'object_name': self.context.__name__,
-                    'marker_type': 'extaudios',
-                    'marker_tag': counter.render()
-                }
-            })
+            output.setdefault('events', []).append(get_json_paragraph_markers_refresh_event(self.context, self.request,
+                                                                                            self, ExtAudiosCounter))
         return output
 
 
--- a/src/pyams_content/component/gallery/zmi/__init__.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/component/gallery/zmi/__init__.py	Wed Apr 11 16:46:31 2018 +0200
@@ -21,8 +21,7 @@
 
 # import interfaces
 from pyams_content.component.gallery.interfaces import IGallery
-from pyams_content.component.gallery.zmi.interfaces import IGalleryMediasAddFields, IGalleryContentsView
-from pyams_content.features.renderer.interfaces import IContentRenderer
+from pyams_content.component.gallery.zmi.interfaces import IGalleryContentsView
 from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
 from pyams_file.interfaces import IFileInfo
 from pyams_form.interfaces.form import IWidgetsPrefixViewletsManager
@@ -33,12 +32,10 @@
 
 # import packages
 from pyams_content.shared.common.zmi import WfSharedContentPermissionMixin
-from pyams_content.features.renderer.zmi import BaseContentRenderer
 from pyams_content.features.renderer.zmi.widget import RendererFieldWidget
 from pyams_form.form import AJAXEditForm
 from pyams_pagelet.pagelet import pagelet_config
 from pyams_template.template import template_config
-from pyams_utils.adapter import adapter_config
 from pyams_utils.url import absolute_url
 from pyams_viewlet.viewlet import viewlet_config, Viewlet
 from pyams_zmi.form import AdminDialogEditForm, AdminDialogDisplayForm
@@ -60,6 +57,8 @@
 class GalleryPropertiesEditForm(AdminDialogEditForm):
     """Gallery properties edit form"""
 
+    prefix = 'gallery_properties.'
+
     legend = _("Update gallery properties")
     icon_css_class = 'fa fa-fw fa-picture-o'
 
@@ -82,8 +81,10 @@
 
     def get_ajax_output(self, changes):
         if 'title' in changes.get(IGallery, ()):
-            return {'status': 'reload',
-                    'location': '#external-files.html'}
+            return {
+                'status': 'reload',
+                'location': '#external-files.html'
+            }
         else:
             return super(GalleryPropertiesAJAXEditForm, self).get_ajax_output(changes)
 
--- a/src/pyams_content/component/gallery/zmi/file.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/component/gallery/zmi/file.py	Wed Apr 11 16:46:31 2018 +0200
@@ -128,9 +128,11 @@
     """Gallery media add form, JSON renderer"""
 
     def get_ajax_output(self, changes):
-        return {'status': 'reload',
-                'location': absolute_url(self.context, self.request, 'get-gallery-medias.html'),
-                'target': '#gallery_medias_{0}'.format(self.context.__name__)}
+        return {
+            'status': 'reload',
+            'location': absolute_url(self.context, self.request, 'get-gallery-medias.html'),
+            'target': '#gallery_medias_{0}'.format(self.context.__name__)
+        }
 
 
 @viewlet_config(name='file.showhide.action', context=IGalleryFile, layer=IPyAMSLayer, view=IGalleryContentsView,
@@ -182,6 +184,8 @@
 class GalleryFilePropertiesEditForm(AdminDialogEditForm):
     """Gallery file properties edit form"""
 
+    prefix = 'file_properties.'
+
     legend = _("Update media properties")
     icon_css_class = 'fa fa-fw fa-picture-o'
     dialog_class = 'modal-large'
@@ -244,21 +248,3 @@
     hint_gravity = 'nw'
 
     url = 'PyAMS_content.galleries.removeMedia'
-
-
-@view_config(name='delete-element.json', context=IGallery, request_type=IPyAMSLayer,
-             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
-def delete_gallery_element(request):
-    """Delete gallery element"""
-    translate = request.localizer.translate
-    name = request.params.get('object_name')
-    if not name:
-        return {'status': 'message',
-                'messagebox': {'status': 'error',
-                               'content': translate(_("No provided object_name argument!"))}}
-    if name not in request.context:
-        return {'status': 'message',
-                'messagebox': {'status': 'error',
-                               'content': translate(_("Given media name doesn't exist!"))}}
-    del request.context[name]
-    return {'status': 'success'}
--- a/src/pyams_content/component/gallery/zmi/paragraph.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/component/gallery/zmi/paragraph.py	Wed Apr 11 16:46:31 2018 +0200
@@ -32,7 +32,7 @@
 # import packages
 from pyams_content.component.gallery.paragraph import Gallery
 from pyams_content.component.paragraph.zmi import BaseParagraphAJAXAddForm, BaseParagraphAJAXEditForm, \
-    BaseParagraphAddMenu, BaseParagraphPropertiesEditForm
+    BaseParagraphAddMenu, BaseParagraphPropertiesEditForm, get_json_paragraph_refresh_event
 from pyams_content.features.renderer.zmi import BaseRenderedContentRenderer
 from pyams_content.features.renderer.zmi.widget import RendererFieldWidget
 from pyams_content.shared.common.zmi import WfSharedContentPermissionMixin
@@ -96,6 +96,8 @@
 class GalleryPropertiesEditForm(BaseParagraphPropertiesEditForm):
     """Gallery properties edit form"""
 
+    prefix = 'gallery_properties.'
+
     legend = _("Edit gallery properties")
     icon_css_class = 'fa fa-fw fa-picture-o'
 
@@ -119,16 +121,10 @@
     def get_ajax_output(self, changes):
         updated = changes.get(IBaseGallery, ())
         if 'title' in updated:
-            return {'status': 'success',
-                    'events': [{
-                        'event': 'myams.refresh',
-                        'options': {
-                            'handler': 'PyAMS_content.paragraphs.refreshParagraph',
-                            'object_name': self.context.__name__,
-                            'title': II18n(self.context).query_attribute('title', request=self.request),
-                            'visible': self.context.visible
-                        }
-                    }]}
+            return {
+                'status': 'success',
+                'events': [get_json_paragraph_refresh_event(self.context, self.request), ]
+            }
         else:
             return super(GalleryPropertiesAJAXEditForm, self).get_ajax_output(changes)
 
--- a/src/pyams_content/component/illustration/zmi/__init__.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/component/illustration/zmi/__init__.py	Wed Apr 11 16:46:31 2018 +0200
@@ -17,6 +17,7 @@
 
 # import interfaces
 from pyams_content.component.illustration.interfaces import IIllustration, IIllustrationTarget
+from pyams_content.component.paragraph import IBaseParagraph
 from pyams_content.component.paragraph.zmi.interfaces import IParagraphContainerTable, IParagraphTitleToolbar
 from pyams_form.interfaces.form import IInnerSubForm, IWidgetsPrefixViewletsManager
 from pyams_skin.layer import IPyAMSLayer
@@ -26,14 +27,16 @@
 from transaction.interfaces import ITransactionManager
 
 # import packages
+from pyams_content.component.paragraph.zmi import get_json_paragraph_markers_refresh_event
 from pyams_content.features.renderer.zmi.widget import RendererFieldWidget
 from pyams_content.skin import pyams_content
 from pyams_form.security import ProtectedFormObjectMixin
+from pyams_skin.event import get_json_form_refresh_event, get_json_widget_refresh_event
 from pyams_skin.viewlet.toolbar import JsToolbarAction
 from pyams_template.template import template_config
 from pyams_utils.adapter import adapter_config
 from pyams_utils.fanstatic import get_resource_path
-from pyams_viewlet.viewlet import viewlet_config, Viewlet
+from pyams_viewlet.viewlet import viewlet_config, Viewlet, EmptyViewlet
 from pyams_zmi.form import InnerAdminEditForm
 from z3c.form import field
 from zope.interface import implementer
@@ -56,14 +59,15 @@
     icon_class = 'fa fa-fw fa-file-image-o'
     icon_hint = _("Illustration")
 
-    def __new__(cls, context, request, view, manager):
-        illustration = IIllustration(context, None)
-        if (not illustration) or not illustration.data:
-            return None
-        for value in illustration.data.values():
-            if value:
-                return Viewlet.__new__(cls)
-        return None
+    marker_type = 'illustration'
+
+    def render(self):
+        illustration = IIllustration(self.context, None)
+        if illustration and illustration.data:
+            for value in illustration.data.values():
+                if value:
+                    return super(ParagraphContainerIllustrationMarker, self).render()
+        return ''
 
 
 @adapter_config(name='illustration', context=(IIllustrationTarget, IPyAMSLayer, IPropertiesEditForm),
@@ -101,35 +105,23 @@
     def get_ajax_output(self, changes):
         output = super(IllustrationPropertiesInnerEditForm, self).get_ajax_output(changes)
         updated = changes.get(IIllustration, ())
-        if ('data' in updated) or ('renderer' in updated):
-            if 'data' in updated:
-                # we have to commit transaction to be able to handle blobs...
-                ITransactionManager(self.context).get().commit()
-            form = IllustrationPropertiesInnerEditForm(self.context, self.request)
-            form.update()
-            illustration = form.getContent()
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'object_id': '{0}_{1}_{2}'.format(
-                        self.context.__class__.__name__,
-                        getattr(illustration, '__name__', 'noname').replace('++', ''),
-                        form.id),
-                    'content': form.render()
-                }
-            })
-            viewlet = ParagraphContainerIllustrationMarker(self.context, self.request, self, None)
-            if viewlet is not None:
-                viewlet.update()
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'PyAMS_content.paragraphs.updateMarkers',
-                    'object_name': self.context.__name__,
-                    'marker_type': 'illustration',
-                    'marker_tag': viewlet.render() if viewlet is not None else ''
-                }
-            })
+        events = output.setdefault('events', [])
+        if 'data' in updated:
+            # we have to commit transaction to be able to handle blobs...
+            ITransactionManager(self.context).get().commit()
+            events.append(get_json_form_refresh_event(self.context, self.request,
+                                                      IllustrationPropertiesInnerEditForm))
+            if IBaseParagraph.providedBy(self.context):
+                if self.getContent().data:
+                    events.append(get_json_paragraph_markers_refresh_event(self.context, self.request, self,
+                                                                           ParagraphContainerIllustrationMarker))
+                else:
+                    events.append(get_json_paragraph_markers_refresh_event(self.context, self.request, self,
+                                                                           EmptyViewlet,
+                                                                           ParagraphContainerIllustrationMarker.marker_type))
+        elif 'renderer' in updated:
+            events.append(get_json_widget_refresh_event(self.context, self.request,
+                                                        IllustrationPropertiesInnerEditForm, 'renderer'))
         return output
 
 
--- a/src/pyams_content/component/illustration/zmi/paragraph.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/component/illustration/zmi/paragraph.py	Wed Apr 11 16:46:31 2018 +0200
@@ -23,7 +23,6 @@
 from pyams_content.component.paragraph.zmi.interfaces import IParagraphInnerEditor, IParagraphContainerView
 from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
 from pyams_form.interfaces.form import IInnerForm, IEditFormButtons
-from pyams_i18n.interfaces import II18n
 from pyams_skin.interfaces.viewlet import IToolbarAddingMenu
 from pyams_skin.layer import IPyAMSLayer
 from transaction.interfaces import ITransactionManager
@@ -32,10 +31,11 @@
 # import packages
 from pyams_content.component.illustration.paragraph import Illustration
 from pyams_content.component.paragraph.zmi import BaseParagraphAJAXAddForm, BaseParagraphAJAXEditForm, \
-    BaseParagraphAddMenu, BaseParagraphPropertiesEditForm
+    BaseParagraphAddMenu, BaseParagraphPropertiesEditForm, get_json_paragraph_refresh_event
 from pyams_content.features.renderer.zmi import BaseRenderedContentRenderer
 from pyams_content.features.renderer.zmi.widget import RendererFieldWidget
 from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.event import get_json_form_refresh_event
 from pyams_utils.adapter import adapter_config
 from pyams_viewlet.viewlet import viewlet_config
 from pyams_zmi.form import AdminDialogAddForm
@@ -97,6 +97,8 @@
 class IllustrationPropertiesEditForm(BaseParagraphPropertiesEditForm):
     """Illustration properties edit form"""
 
+    prefix = 'illustration_properties.'
+
     legend = _("Edit illustration properties")
     dialog_class = 'modal-large'
     icon_css_class = 'fa fa-fw fa-file-image-o'
@@ -121,15 +123,7 @@
     def get_ajax_output(self, changes):
         output = super(IllustrationPropertiesAJAXEditForm, self).get_ajax_output(changes)
         if 'title' in changes.get(IIllustration, ()):
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'PyAMS_content.paragraphs.refreshParagraph',
-                    'object_name': self.context.__name__,
-                    'title': II18n(self.context).query_attribute('title', request=self.request),
-                    'visible': self.context.visible
-                }
-            })
+            output.setdefault('events', []).append(get_json_paragraph_refresh_event(self.context, self.request))
         return output
 
 
@@ -158,31 +152,13 @@
         output = super(IllustrationInnerAJAXEditForm, self).get_ajax_output(changes)
         updated = changes.get(IIllustration, ())
         if 'title' in updated:
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'PyAMS_content.paragraphs.refreshParagraph',
-                    'object_name': self.context.__name__,
-                    'title': II18n(self.context).query_attribute('title', request=self.request),
-                    'visible': self.context.visible
-                }
-            })
+            output.setdefault('events', []).append(get_json_paragraph_refresh_event(self.context, self.request))
         updated = changes.get(IIllustrationParagraph, ())
         if 'data' in updated:
             # we have to commit transaction to be able to handle blobs...
             ITransactionManager(self.context).get().commit()
-            form = IllustrationInnerEditForm(self.context, self.request)
-            form.update()
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'object_id': '{0}_{1}_{2}'.format(
-                        self.context.__class__.__name__,
-                        getattr(form.getContent(), '__name__', 'noname').replace('++', ''),
-                        form.id),
-                    'content': form.render()
-                }
-            })
+            output.setdefault('events', []).append(get_json_form_refresh_event(self.context, self.request,
+                                                                               IllustrationInnerEditForm))
         return output
 
 
--- a/src/pyams_content/component/links/zmi/__init__.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/component/links/zmi/__init__.py	Wed Apr 11 16:46:31 2018 +0200
@@ -9,6 +9,7 @@
 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
 # FOR A PARTICULAR PURPOSE.
 #
+from pyams_content.component.paragraph.zmi import get_json_paragraph_markers_refresh_event
 
 __docformat__ = 'restructuredtext'
 
@@ -55,6 +56,8 @@
     icon_class = 'fa fa-fw fa-external-link-square fa-rotate-90'
     icon_hint = _("Internal links")
 
+    marker_type = 'internal-links'
+
     @property
     def count(self):
         return len([file for file in IAssociationContainer(self.context).values()
@@ -105,17 +108,8 @@
     def get_ajax_output(self, changes):
         output = super(InternalLinkAJAXAddForm, self).get_ajax_output(changes)
         if output:
-            counter = InternalLinksCounter(self.context, self.request, self, None)
-            counter.update()
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'PyAMS_content.paragraphs.updateMarkers',
-                    'object_name': self.context.__name__,
-                    'marker_type': 'internal-links',
-                    'marker_tag': counter.render()
-                }
-            })
+            output.setdefault('events', []).append(get_json_paragraph_markers_refresh_event(self.context, self.request,
+                                                                                            self, InternalLinksCounter))
         return output
 
 
@@ -123,6 +117,8 @@
 class InternalLinkPropertiesEditForm(AdminDialogEditForm):
     """Internal link properties edit form"""
 
+    prefix = 'internallink_properties.'
+
     legend = _("Edit internal link properties")
     icon_css_class = 'fa fa-fw fa-external-link-square fa-rotate-90'
 
@@ -163,6 +159,8 @@
     icon_class = 'fa fa-fw fa-external-link'
     icon_hint = _("External links")
 
+    marker_type = 'external-links'
+
     @property
     def count(self):
         return len([file for file in IAssociationContainer(self.context).values()
@@ -213,17 +211,8 @@
     def get_ajax_output(self, changes):
         output = super(ExternalLinkAJAXAddForm, self).get_ajax_output(changes)
         if output:
-            counter = ExternalLinksCounter(self.context, self.request, self, None)
-            counter.update()
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'PyAMS_content.paragraphs.updateMarkers',
-                    'object_name': self.context.__name__,
-                    'marker_type': 'external-links',
-                    'marker_tag': counter.render()
-                }
-            })
+            output.setdefault('events', []).append(get_json_paragraph_markers_refresh_event(self.context, self.request,
+                                                                                            self, ExternalLinksCounter))
         return output
 
 
@@ -231,6 +220,8 @@
 class ExternalLinkPropertiesEditForm(AdminDialogEditForm):
     """External link properties edit form"""
 
+    prefix = 'externallink_properties.'
+
     legend = _("Edit external link properties")
     icon_css_class = 'fa fa-fw fa-external-link'
 
@@ -271,6 +262,8 @@
     icon_class = 'fa fa-fw fa-envelope-o'
     icon_hint = _("Mailto links")
 
+    marker_type = 'mailto-links'
+
     @property
     def count(self):
         return len([file for file in IAssociationContainer(self.context).values()
@@ -321,17 +314,8 @@
     def get_ajax_output(self, changes):
         output = super(MailtoLinkAJAXAddForm, self).get_ajax_output(changes)
         if output:
-            counter = MailtoLinksCounter(self.context, self.request, self, None)
-            counter.update()
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'PyAMS_content.paragraphs.updateMarkers',
-                    'object_name': self.context.__name__,
-                    'marker_type': 'mailto-links',
-                    'marker_tag': counter.render()
-                }
-            })
+            output.setdefault('events', []).append(get_json_paragraph_markers_refresh_event(self.context, self.request,
+                                                                                            self, MailtoLinksCounter))
         return output
 
 
@@ -339,6 +323,8 @@
 class MailtoLinkPropertiesEditForm(AdminDialogEditForm):
     """Mailto link properties edit form"""
 
+    prefix = 'mailtolink_properties.'
+
     legend = _("Edit mailto link properties")
     icon_css_class = 'fa fa-fw fa-envelope-o'
 
@@ -358,8 +344,7 @@
     """Mailto link properties edit form, JSON renderer"""
 
     def get_ajax_output(self, changes):
-        if ('title' in changes.get(IBaseLink, ())) or \
-           ('reference' in changes.get(IMailtoLink, ())):
+        if ('title' in changes.get(IBaseLink, ())) or changes.get(IMailtoLink, ()):
             return self.get_associations_table()
         else:
             return super(MailtoLinkPropertiesAJAXEditForm, self).get_ajax_output(changes)
--- a/src/pyams_content/component/paragraph/interfaces/keynumber.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/component/paragraph/interfaces/keynumber.py	Wed Apr 11 16:46:31 2018 +0200
@@ -9,7 +9,6 @@
 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
 # FOR A PARTICULAR PURPOSE.
 #
-from zope.schema._bootstrapfields import TextLine
 
 __docformat__ = 'restructuredtext'
 
--- a/src/pyams_content/component/paragraph/keynumber.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/component/paragraph/keynumber.py	Wed Apr 11 16:46:31 2018 +0200
@@ -21,7 +21,7 @@
 from pyams_content.component.paragraph.interfaces.keynumber import IKeyNumber, IKeyNumberContainer, \
     IKeyNumberContainerTarget, KEYNUMBER_CONTAINER_KEY, IKeyNumberParagraph, KEYNUMBER_PARAGRAPH_TYPE, \
     KEYNUMBER_PARAGRAPH_RENDERERS
-from pyams_content.features.checker.interfaces import IContentChecker, MISSING_VALUE, MISSING_LANG_VALUE, ERROR_VALUE
+from pyams_content.features.checker.interfaces import IContentChecker, MISSING_VALUE, MISSING_LANG_VALUE
 from pyams_form.interfaces.form import IFormContextPermissionChecker
 from pyams_i18n.interfaces import II18n, II18nManager, INegotiator
 from zope.annotation.interfaces import IAnnotations
--- a/src/pyams_content/component/paragraph/zmi/__init__.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/component/paragraph/zmi/__init__.py	Wed Apr 11 16:46:31 2018 +0200
@@ -35,6 +35,8 @@
 from pyams_form.help import FormHelp
 from pyams_form.security import ProtectedFormObjectMixin
 from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.event import get_json_switched_table_refresh_event
+from pyams_skin.table import get_element_id
 from pyams_skin.viewlet.menu import MenuItem, MenuDivider
 from pyams_skin.viewlet.toolbar import ToolbarMenuItem
 from pyams_utils.adapter import adapter_config
@@ -69,6 +71,8 @@
 class DefaultParagraphsEditForm(AdminDialogEditForm):
     """Default paragraphs edit form"""
 
+    prefix = 'default_paragraphs.'
+
     legend = _("Paragraphs types")
 
     fields = field.Fields(IParagraphFactorySettings)
@@ -96,6 +100,58 @@
 # Base paragraph forms
 #
 
+def get_json_paragraph_refresh_event(context, request):
+    """Get JSON response value for paragraph refresh event"""
+    parent = get_parent(context, IParagraphContainerTarget)
+    return {
+        'event': 'myams.refresh',
+        'options': {
+            'handler': 'PyAMS_content.paragraphs.refreshParagraph',
+            'object_id': get_element_id(ParagraphContainerBaseTable, context, parent),
+            'title': II18n(context).query_attribute('title', request=request),
+            'visible': context.visible
+        }
+    }
+
+
+def get_json_paragraph_toolbar_refresh_event(context, request, table_factory=None, viewlet_factory=None):
+    """Get JSON response value for paragraph toolbar refresh event"""
+    if table_factory is None:
+        from pyams_content.component.paragraph.zmi.container import ParagraphContainerTable as table_factory
+    if viewlet_factory is None:
+        from pyams_content.component.paragraph.zmi.container import ParagraphTitleToolbarViewletManager as viewlet_factory
+
+    parent = get_parent(context, IParagraphContainerTarget)
+    table = table_factory(context, request)
+    viewlet = viewlet_factory(context, request, table)
+    viewlet.update()
+    return {
+        'event': 'myams.refresh',
+        'options': {
+            'handler': 'PyAMS_content.paragraphs.updateToolbar',
+            'object_id': get_element_id(ParagraphContainerBaseTable, context, parent),
+            'toolbar_tag': viewlet.render()
+        }
+    }
+
+
+def get_json_paragraph_markers_refresh_event(context, request, form, viewlet_factory, marker_type=None):
+    """Get JSON response value for paragraph markers refresh event"""
+    parent = get_parent(context, IParagraphContainerTarget)
+    marker = viewlet_factory(context, request, form, None)
+    if marker is not None:
+        marker.update()
+        return {
+            'event': 'myams.refresh',
+            'options': {
+                'handler': 'PyAMS_content.paragraphs.updateMarkers',
+                'object_id': get_element_id(ParagraphContainerBaseTable, context, parent),
+                'marker_type': marker_type or marker.marker_type,
+                'marker_tag': marker.render()
+            }
+        }
+
+
 class BaseParagraphAddMenu(ProtectedFormObjectMixin, ToolbarMenuItem):
     """Base paragraph add menu"""
 
@@ -133,26 +189,17 @@
             table_factory = ParagraphContainerTable
         else:
             table_factory = ParagraphContainerBaseTable
-        table = table_factory(self.context, self.request)
-        table.update()
-        return {'status': 'success',
-                'message': self.request.localizer.translate(_("Paragraph was correctly added.")),
-                'events': [{
-                    'event': 'myams.refresh',
-                    'options': {
-                        'handler': 'MyAMS.skin.refreshSwitchedTable',
-                        'object_id': table.id,
-                        'table': table.render()
-                    }
-                }]}
+        return {
+            'status': 'success',
+            'message': self.request.localizer.translate(_("Paragraph was correctly added.")),
+            'events': [get_json_switched_table_refresh_event(self.context, self.request, table_factory)]
+        }
 
 
 class BaseParagraphPropertiesEditForm(AdminDialogEditForm):
     """Base paragraph edit form"""
 
-    @property
-    def prefix(self):
-        return 'paragraph_{0}_form.'.format(self.context.__name__)
+    prefix = 'paragraph.'
 
     @property
     def title(self):
@@ -173,13 +220,6 @@
     def get_ajax_output(self, changes):
         output = super(BaseParagraphAJAXEditForm, self).get_ajax_output(changes)
         if 'title' in changes.get(IBaseParagraph, ()):
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'PyAMS_content.paragraphs.refreshParagraph',
-                    'object_name': self.context.__name__,
-                    'title': II18n(self.context).query_attribute('title', request=self.request),
-                    'visible': self.context.visible
-                }
-            })
+            output.setdefault('events', []).append(
+                get_json_paragraph_refresh_event(self.context, self.request))
         return output
--- a/src/pyams_content/component/paragraph/zmi/contact.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/component/paragraph/zmi/contact.py	Wed Apr 11 16:46:31 2018 +0200
@@ -22,7 +22,6 @@
 from pyams_content.component.paragraph.zmi.interfaces import IParagraphContainerView, IParagraphInnerEditor
 from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
 from pyams_form.interfaces.form import IInnerForm, IEditFormButtons
-from pyams_i18n.interfaces import II18n
 from pyams_skin.interfaces.viewlet import IToolbarAddingMenu
 from pyams_skin.layer import IPyAMSLayer
 from transaction.interfaces import ITransactionManager
@@ -31,10 +30,11 @@
 # import packages
 from pyams_content.component.paragraph.contact import ContactParagraph
 from pyams_content.component.paragraph.zmi import BaseParagraphAddMenu, BaseParagraphAJAXAddForm, \
-    BaseParagraphPropertiesEditForm, BaseParagraphAJAXEditForm
+    BaseParagraphPropertiesEditForm, BaseParagraphAJAXEditForm, get_json_paragraph_refresh_event
 from pyams_content.features.renderer.zmi import BaseRenderedContentRenderer
 from pyams_content.features.renderer.zmi.widget import RendererFieldWidget
 from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.event import get_json_form_refresh_event
 from pyams_utils.adapter import adapter_config
 from pyams_viewlet.viewlet import viewlet_config
 from pyams_zmi.form import AdminDialogAddForm
@@ -92,6 +92,8 @@
 class ContactParagraphPropertiesEditForm(BaseParagraphPropertiesEditForm):
     """Contact paragraph properties edit form"""
 
+    prefix = 'contact_properties.'
+
     legend = _("Edit contact card properties")
     icon_css_class = 'fa fa-fw fa-id-card-o'
 
@@ -138,31 +140,14 @@
         output = super(ContactParagraphInnerAJAXEditForm, self).get_ajax_output(changes)
         updated = changes.get(IBaseParagraph, ())
         if 'title' in updated:
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'PyAMS_content.paragraphs.refreshParagraph',
-                    'object_name': self.context.__name__,
-                    'title': II18n(self.context).query_attribute('title', request=self.request),
-                    'visible': self.context.visible
-                }
-            })
+            output.setdefault('events', []).append(get_json_paragraph_refresh_event(self.context, self.request))
         updated = changes.get(IContactParagraph, ())
         if ('photo' in updated) or ('renderer' in updated):
             # we have to commit transaction to be able to handle blobs...
-            ITransactionManager(self.context).get().commit()
-            form = ContactParagraphInnerEditForm(self.context, self.request)
-            form.update()
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'object_id': '{0}_{1}_{2}'.format(
-                        self.context.__class__.__name__,
-                        getattr(form.getContent(), '__name__', 'noname').replace('++', ''),
-                        form.id),
-                    'content': form.render()
-                }
-            })
+            if 'photo' in updated:
+                ITransactionManager(self.context).get().commit()
+            output.setdefault('events', []).append(get_json_form_refresh_event(self.context, self.request,
+                                                                               ContactParagraphInnerEditForm))
         return output
 
 
--- a/src/pyams_content/component/paragraph/zmi/container.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/component/paragraph/zmi/container.py	Wed Apr 11 16:46:31 2018 +0200
@@ -84,7 +84,8 @@
 class ParagraphContainerBaseTable(ProtectedFormObjectMixin, BaseTable):
     """Paragraphs container table"""
 
-    id = 'paragraphs_list'
+    prefix = 'paragraphs'
+
     hide_header = True
     sortOn = None
 
@@ -108,7 +109,7 @@
             'data-ams-post-reload': 'PyAMS_content.paragraphs.postReload',
             'data-ams-tablednd-drag-handle': 'td.sorter',
             'data-ams-tablednd-drop-target': 'set-paragraphs-order.json',
-            'data-ams-visibility-target': 'switch-paragraph-visibility.json'
+            'data-ams-visibility-switcher': 'switch-paragraph-visibility.json'
         })
         return attributes
 
@@ -267,6 +268,8 @@
     """Paragraph container base counter viewlet"""
 
     weight = 0
+
+    marker_type = None
     count = None
 
 
--- a/src/pyams_content/component/paragraph/zmi/frame.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/component/paragraph/zmi/frame.py	Wed Apr 11 16:46:31 2018 +0200
@@ -34,12 +34,13 @@
 from pyams_content.component.association.zmi import AssociationsTable
 from pyams_content.component.paragraph.frame import FrameParagraph
 from pyams_content.component.paragraph.zmi import BaseParagraphAJAXAddForm, BaseParagraphAJAXEditForm, \
-    BaseParagraphAddMenu, BaseParagraphPropertiesEditForm
+    BaseParagraphAddMenu, BaseParagraphPropertiesEditForm, get_json_paragraph_toolbar_refresh_event
 from pyams_content.component.paragraph.zmi.container import ParagraphContainerTable, \
     ParagraphTitleToolbarViewletManager
 from pyams_content.features.renderer.zmi import BaseRenderedContentRenderer
 from pyams_content.features.renderer.zmi.widget import RendererFieldWidget
 from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.event import get_json_switched_table_refresh_event, get_json_widget_refresh_event
 from pyams_utils.adapter import adapter_config, ContextRequestAdapter
 from pyams_utils.traversing import get_parent
 from pyams_viewlet.viewlet import viewlet_config
@@ -127,6 +128,8 @@
 class FrameParagraphPropertiesEditForm(BaseParagraphPropertiesEditForm):
     """Framed text paragraph properties edit form"""
 
+    prefix = 'frame_properties.'
+
     legend = _("Edit framed text paragraph properties")
     dialog_class = 'modal-large'
     icon_css_class = 'fa fa-fw fa-list-alt'
@@ -159,28 +162,12 @@
         if 'body' in changes.get(IFrameParagraph, ()):
             # refresh associations count markers
             parent = get_parent(self.context, IAssociationTarget)
-            table = ParagraphContainerTable(parent, self.request)
-            viewlet = ParagraphTitleToolbarViewletManager(parent, self.request, table)
-            viewlet.update()
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'PyAMS_content.paragraphs.updateToolbar',
-                    'object_name': self.context.__name__,
-                    'toolbar_tag': viewlet.render()
-                }
-            })
+            output.setdefault('events', []).append(
+                get_json_paragraph_toolbar_refresh_event(parent, self.request,
+                                                         ParagraphContainerTable, ParagraphTitleToolbarViewletManager))
             # refresh associations table
-            associations_table = AssociationsTable(self.context, self.request)
-            associations_table.update()
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'PyAMS_content.associations.refreshAssociations',
-                    'object_id': associations_table.id,
-                    'table': associations_table.render()
-                }
-            })
+            output.setdefault('events', []).append(
+                get_json_switched_table_refresh_event(self.context, self.request, AssociationsTable))
         return output
 
 
@@ -209,20 +196,8 @@
         output = super(FrameParagraphInnerAJAXEditForm, self).get_ajax_output(changes)
         updated = changes.get(IFrameParagraph, ())
         if 'renderer' in updated:
-            form = FrameParagraphInnerEditForm(self.context, self.request)
-            form.update()
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'MyAMS.skin.refreshWidget',
-                    'parent_id': '{0}_{1}_{2}'.format(
-                        self.context.__class__.__name__,
-                        getattr(form.getContent(), '__name__', 'noname').replace('++', ''),
-                        form.id),
-                    'widget_name': form.widgets['renderer'].name,
-                    'content': form.widgets['renderer'].render()
-                }
-            })
+            output.setdefault('events', []).append(
+                get_json_widget_refresh_event(self.context, self.request, FrameParagraphInnerEditForm, 'renderer'))
         return output
 
 
--- a/src/pyams_content/component/paragraph/zmi/header.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/component/paragraph/zmi/header.py	Wed Apr 11 16:46:31 2018 +0200
@@ -22,7 +22,6 @@
 from pyams_content.component.paragraph.zmi.interfaces import IParagraphInnerEditor, IParagraphContainerView
 from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
 from pyams_form.interfaces.form import IInnerForm, IEditFormButtons
-from pyams_i18n.interfaces import II18n
 from pyams_skin.interfaces.viewlet import IToolbarAddingMenu
 from pyams_skin.layer import IPyAMSLayer
 from z3c.form.interfaces import INPUT_MODE
@@ -30,10 +29,11 @@
 # import packages
 from pyams_content.component.paragraph.header import HeaderParagraph
 from pyams_content.component.paragraph.zmi import BaseParagraphAJAXAddForm, BaseParagraphAJAXEditForm, \
-    BaseParagraphAddMenu, BaseParagraphPropertiesEditForm
+    BaseParagraphAddMenu, BaseParagraphPropertiesEditForm, get_json_paragraph_refresh_event
 from pyams_content.features.renderer.zmi import BaseRenderedContentRenderer
 from pyams_content.features.renderer.zmi.widget import RendererFieldWidget
 from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.event import get_json_widget_refresh_event
 from pyams_utils.adapter import adapter_config
 from pyams_viewlet.viewlet import viewlet_config
 from pyams_zmi.form import AdminDialogAddForm
@@ -90,6 +90,8 @@
 class HeaderParagraphPropertiesEditForm(BaseParagraphPropertiesEditForm):
     """Header paragraph properties edit form"""
 
+    prefix = 'header_properties.'
+
     legend = _("Edit header paragraph properties")
     icon_css_class = 'fa fa-fw fa-header'
 
@@ -113,15 +115,7 @@
     def get_ajax_output(self, changes):
         output = super(HeaderParagraphPropertiesAJAXEditForm, self).get_ajax_output(changes)
         if 'header' in changes.get(IHeaderParagraph, ()):
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'PyAMS_content.paragraphs.refreshParagraph',
-                    'object_name': self.context.__name__,
-                    'title': II18n(self.context).query_attribute('title', request=self.request),
-                    'visible': self.context.visible
-                }
-            })
+            output.setdefault('events', []).append(get_json_paragraph_refresh_event(self.context, self.request))
         return output
 
 
@@ -152,21 +146,11 @@
     def get_ajax_output(self, changes):
         output = super(HeaderParagraphInnerAJAXEditForm, self).get_ajax_output(changes)
         updated = changes.get(IHeaderParagraph, ())
+        if 'header' in updated:
+            output.setdefault('events', []).append(get_json_paragraph_refresh_event(self.context, self.request))
         if 'renderer' in updated:
-            form = HeaderParagraphInnerEditForm(self.context, self.request)
-            form.update()
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'MyAMS.skin.refreshWidget',
-                    'parent_id': '{0}_{1}_{2}'.format(
-                        self.context.__class__.__name__,
-                        getattr(form.getContent(), '__name__', 'noname').replace('++', ''),
-                        form.id),
-                    'widget_name': form.widgets['renderer'].name,
-                    'content': form.widgets['renderer'].render()
-                }
-            })
+            output.setdefault('events', []).append(get_json_widget_refresh_event(self.context, self.request,
+                                                                                 HeaderParagraphInnerEditForm, 'renderer'))
         return output
 
 
--- a/src/pyams_content/component/paragraph/zmi/html.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/component/paragraph/zmi/html.py	Wed Apr 11 16:46:31 2018 +0200
@@ -36,11 +36,10 @@
 from pyams_content.component.association.zmi import AssociationsTable
 from pyams_content.component.paragraph.html import HTMLParagraph, RawParagraph
 from pyams_content.component.paragraph.zmi import BaseParagraphAJAXAddForm, BaseParagraphAJAXEditForm, \
-    BaseParagraphAddMenu, BaseParagraphPropertiesEditForm
-from pyams_content.component.paragraph.zmi.container import ParagraphContainerTable, \
-    ParagraphTitleToolbarViewletManager
+    BaseParagraphAddMenu, BaseParagraphPropertiesEditForm, get_json_paragraph_toolbar_refresh_event
 from pyams_form.security import ProtectedFormObjectMixin
 from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.event import get_json_switched_table_refresh_event
 from pyams_skin.viewlet.menu import MenuDivider
 from pyams_template.template import template_config
 from pyams_utils.adapter import adapter_config
@@ -119,6 +118,8 @@
 class RawParagraphPropertiesEditForm(BaseParagraphPropertiesEditForm):
     """Raw HTML paragraph properties edit form"""
 
+    prefix = 'raw_properties.'
+
     legend = _("Edit raw HTML paragraph properties")
     icon_css_class = 'fa fa-fw fa-code'
 
@@ -234,6 +235,8 @@
 class HTMLParagraphPropertiesEditForm(BaseParagraphPropertiesEditForm):
     """Rich text paragraph properties edit form"""
 
+    prefix = 'html_properties.'
+
     legend = _("Edit rich text paragraph properties")
     dialog_class = 'modal-max'
     icon_css_class = 'fa fa-fw fa-html5'
@@ -264,28 +267,11 @@
         if 'body' in changes.get(IHTMLParagraph, ()):
             # refresh associations count markers
             parent = get_parent(self.context, IAssociationTarget)
-            table = ParagraphContainerTable(parent, self.request)
-            viewlet = ParagraphTitleToolbarViewletManager(parent, self.request, table)
-            viewlet.update()
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'PyAMS_content.paragraphs.updateToolbar',
-                    'object_name': self.context.__name__,
-                    'toolbar_tag': viewlet.render()
-                }
-            })
+            output.setdefault('events', []).append(
+                get_json_paragraph_toolbar_refresh_event(parent, self.request))
             # refresh associations table
-            associations_table = AssociationsTable(self.context, self.request)
-            associations_table.update()
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'PyAMS_content.associations.refreshAssociations',
-                    'object_id': associations_table.id,
-                    'table': associations_table.render()
-                }
-            })
+            output.setdefault('events', []).append(
+                get_json_switched_table_refresh_event(self.context, self.request, AssociationsTable))
         return output
 
 
@@ -315,28 +301,11 @@
         if 'body' in changes.get(IHTMLParagraph, ()):
             # refresh associations count markers
             parent = get_parent(self.context, IAssociationTarget)
-            table = ParagraphContainerTable(parent, self.request)
-            viewlet = ParagraphTitleToolbarViewletManager(parent, self.request, table)
-            viewlet.update()
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'PyAMS_content.paragraphs.updateToolbar',
-                    'object_name': self.context.__name__,
-                    'toolbar_tag': viewlet.render()
-                }
-            })
+            output.setdefault('events', []).append(
+                get_json_paragraph_toolbar_refresh_event(parent, self.request))
             # refresh associations table
-            associations_table = AssociationsTable(self.context, self.request)
-            associations_table.update()
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'PyAMS_content.associations.refreshAssociations',
-                    'object_id': associations_table.id,
-                    'table': associations_table.render()
-                }
-            })
+            output.setdefault('events', []).append(
+                get_json_switched_table_refresh_event(self.context, self.request, AssociationsTable))
         return output
 
 
--- a/src/pyams_content/component/paragraph/zmi/keynumber.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/component/paragraph/zmi/keynumber.py	Wed Apr 11 16:46:31 2018 +0200
@@ -43,6 +43,8 @@
 from pyams_i18n.column import I18nAttrColumn
 from pyams_pagelet.pagelet import pagelet_config
 from pyams_skin.container import switch_element_visibility
+from pyams_skin.event import get_json_widget_refresh_event, get_json_switched_table_refresh_event, \
+    get_json_table_row_refresh_event
 from pyams_skin.table import BaseTable, SorterColumn, I18nColumn, VisibilitySwitcherColumn, TrashColumn
 from pyams_skin.viewlet.toolbar import ToolbarAction
 from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter
@@ -109,6 +111,8 @@
 class KeyNumberParagraphPropertiesEditForm(BaseParagraphPropertiesEditForm):
     """Key number paragraph properties edit form"""
 
+    prefix = 'keynumbers_properties.'
+
     @property
     def title(self):
         content = get_parent(self.context, IWfSharedContent)
@@ -155,20 +159,8 @@
         output = super(KeyNumberParagraphInnerAJAXEditForm, self).get_ajax_output(changes)
         updated = changes.get(IKeyNumberParagraph, ())
         if 'renderer' in updated:
-            form = KeyNumberParagraphInnerEditForm(self.context, self.request)
-            form.update()
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'MyAMS.skin.refreshWidget',
-                    'parent_id': '{0}_{1}_{2}'.format(
-                        self.context.__class__.__name__,
-                        getattr(form.getContent(), '__name__', 'noname').replace('++', ''),
-                        form.id),
-                    'widget_name': form.widgets['renderer'].name,
-                    'content': form.widgets['renderer'].render()
-                }
-            })
+            output.setdefault('events', []).append(
+                get_json_widget_refresh_event(self.context, self.request, KeyNumberParagraphInnerEditForm, 'renderer'))
         return output
 
 
@@ -188,9 +180,7 @@
 class KeyNumbersTable(ProtectedFormObjectMixin, BaseTable):
     """Key numbers view inner table"""
 
-    @property
-    def id(self):
-        return 'keynumbers_{0}_list'.format(self.context.__name__)
+    prefix = 'keynumbers'
 
     hide_header = True
     sortOn = None
@@ -211,13 +201,8 @@
             'data-ams-location': absolute_url(IKeyNumberContainer(self.context), self.request),
             'data-ams-tablednd-drag-handle': 'td.sorter',
             'data-ams-tablednd-drop-target': 'set-keynumbers-order.json',
-            'data-ams-visibility-target': 'switch-keynumber-visibility.json'
+            'data-ams-visibility-switcher': 'switch-keynumber-visibility.json'
         }
-        attributes.setdefault('tr', {}).update({
-            'id': lambda x, col: 'keynumber_{0}::{1}'.format(get_parent(x, IKeyNumberContainerTarget).__name__,
-                                                             x.__name__),
-            'data-ams-delete-target': 'delete-element.json'
-        })
         return attributes
 
     @reify
@@ -345,19 +330,10 @@
     """Key number add form, JSON renderer"""
 
     def get_ajax_output(self, changes):
-        table = KeyNumbersTable(self.context, self.request)
-        table.update()
         return {
             'status': 'success',
             'message': self.request.localizer.translate(_("Key number was correctly added")),
-            'events': [{
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'MyAMS.skin.refreshSwitchedTable',
-                    'object_id': table.id,
-                    'table': table.render()
-                }
-            }]
+            'events': [get_json_switched_table_refresh_event(self.context, self.request, KeyNumbersTable), ]
         }
 
 
@@ -365,6 +341,8 @@
 class KeyNumberPropertiesEditForm(AdminDialogEditForm):
     """Key number properties edit form"""
 
+    prefix = 'keynumber_properties.'
+
     legend = _("Edit keynumber properties")
     icon_css_class = 'fa fa-fw fa-list-ol'
 
@@ -383,16 +361,6 @@
         updated = changes.get(IKeyNumber, ())
         if updated:
             target = get_parent(self.context, IKeyNumberContainerTarget)
-            table = KeyNumbersTable(target, self.request)
-            table.update()
-            row = table.setUpRow(self.context)
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'MyAMS.skin.refreshRow',
-                    'object_id': 'keynumber_{0}::{1}'.format(target.__name__,
-                                                             self.context.__name__),
-                    'row': table.renderRow(row)
-                }
-            })
+            output.setdefault('events', []).append(
+                get_json_table_row_refresh_event(target, self.request, KeyNumbersTable, self.context))
         return output
--- a/src/pyams_content/component/paragraph/zmi/keypoint.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/component/paragraph/zmi/keypoint.py	Wed Apr 11 16:46:31 2018 +0200
@@ -22,7 +22,6 @@
 from pyams_content.component.paragraph.zmi.interfaces import IParagraphInnerEditor, IParagraphContainerView
 from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
 from pyams_form.interfaces.form import IInnerForm, IEditFormButtons
-from pyams_i18n.interfaces import II18n
 from pyams_skin.interfaces.viewlet import IToolbarAddingMenu
 from pyams_skin.layer import IPyAMSLayer
 from z3c.form.interfaces import INPUT_MODE
@@ -30,10 +29,11 @@
 # import packages
 from pyams_content.component.paragraph.keypoint import KeypointsParagraph
 from pyams_content.component.paragraph.zmi import BaseParagraphAJAXAddForm, BaseParagraphAJAXEditForm, \
-    BaseParagraphAddMenu, BaseParagraphPropertiesEditForm
+    BaseParagraphAddMenu, BaseParagraphPropertiesEditForm, get_json_paragraph_refresh_event
 from pyams_content.features.renderer.zmi import BaseRenderedContentRenderer
 from pyams_content.features.renderer.zmi.widget import RendererFieldWidget
 from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.event import get_json_widget_refresh_event
 from pyams_utils.adapter import adapter_config
 from pyams_viewlet.viewlet import viewlet_config
 from pyams_zmi.form import AdminDialogAddForm
@@ -90,6 +90,8 @@
 class KeypointsParagraphPropertiesEditForm(BaseParagraphPropertiesEditForm):
     """Key points paragraph properties edit form"""
 
+    prefix = 'keypoints_properties.'
+
     legend = _("Edit key points paragraph properties")
     icon_css_class = 'fa fa-fw fa-key'
 
@@ -113,15 +115,8 @@
     def get_ajax_output(self, changes):
         output = super(KeypointsParagraphPropertiesAJAXEditForm, self).get_ajax_output(changes)
         if 'body' in changes.get(IKeypointsParagraph, ()):
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'PyAMS_content.paragraphs.refreshParagraph',
-                    'object_name': self.context.__name__,
-                    'title': II18n(self.context).query_attribute('title', request=self.request),
-                    'visible': self.context.visible
-                }
-            })
+            output.setdefault('events', []).append(
+                get_json_paragraph_refresh_event(self.context, self.request))
         return output
 
 
@@ -155,18 +150,8 @@
         if 'renderer' in updated:
             form = KeypointsParagraphInnerEditForm(self.context, self.request)
             form.update()
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'MyAMS;skin.refreshWidget',
-                    'parent_id': '{0}_{1}_{2}'.format(
-                        self.context.__class__.__name__,
-                        getattr(form.getContent(), '__name__', 'noname').replace('++', ''),
-                        form.id),
-                    'widget_name': form.widgets['renderer'].name,
-                    'content': form.widgets['renderer'].render()
-                }
-            })
+            output.setdefault('events', []).append(
+                get_json_widget_refresh_event(self.context, self.request, KeypointsParagraphInnerEditForm, 'renderer'))
         return output
 
 
--- a/src/pyams_content/component/paragraph/zmi/milestone.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/component/paragraph/zmi/milestone.py	Wed Apr 11 16:46:31 2018 +0200
@@ -29,7 +29,6 @@
 from pyams_i18n.interfaces import II18n
 from pyams_skin.interfaces.viewlet import IToolbarAddingMenu, IWidgetTitleViewletManager
 from pyams_skin.layer import IPyAMSLayer
-from pyams_utils.interfaces import MANAGE_PERMISSION
 from z3c.form.interfaces import INPUT_MODE
 from z3c.table.interfaces import IValues, IColumn
 
@@ -39,22 +38,22 @@
     BaseParagraphPropertiesEditForm, BaseParagraphAJAXEditForm
 from pyams_content.features.renderer.zmi import BaseRenderedContentRenderer
 from pyams_content.features.renderer.zmi.widget import RendererFieldWidget
-from pyams_content.skin import pyams_content
 from pyams_form.form import AJAXAddForm, AJAXEditForm
 from pyams_form.security import ProtectedFormObjectMixin
 from pyams_i18n.column import I18nAttrColumn
 from pyams_pagelet.pagelet import pagelet_config
-from pyams_skin.table import BaseTable, SorterColumn, JsActionColumn, I18nColumn, TrashColumn
+from pyams_skin.container import switch_element_visibility
+from pyams_skin.event import get_json_switched_table_refresh_event, get_json_table_row_refresh_event, \
+    get_json_widget_refresh_event
+from pyams_skin.table import BaseTable, SorterColumn, I18nColumn, TrashColumn, VisibilitySwitcherColumn
 from pyams_skin.viewlet.toolbar import ToolbarAction
-from pyams_template.template import template_config
 from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter
-from pyams_utils.fanstatic import get_resource_path
 from pyams_utils.traversing import get_parent
 from pyams_utils.url import absolute_url
 from pyams_viewlet.viewlet import viewlet_config
-from pyams_zmi.form import AdminDialogAddForm, InnerAdminDisplayForm, AdminDialogEditForm
+from pyams_zmi.form import AdminDialogAddForm, AdminDialogEditForm
+from pyams_zmi.zmi.table import InnerTableView
 from pyramid.decorator import reify
-from pyramid.exceptions import NotFound
 from pyramid.view import view_config
 from z3c.form import field, button
 from z3c.table.column import GetAttrColumn
@@ -113,6 +112,8 @@
 class MilestoneParagraphPropertiesEditForm(BaseParagraphPropertiesEditForm):
     """Milestone paragraph properties edit form"""
 
+    prefix = 'milestones_properties.'
+
     @property
     def title(self):
         content = get_parent(self.context, IWfSharedContent)
@@ -161,18 +162,9 @@
         if 'renderer' in updated:
             form = MilestoneParagraphInnerEditForm(self.context, self.request)
             form.update()
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'MyAMS.skin.refreshWidget',
-                    'parent_id': '{0}_{1}_{2}'.format(
-                        self.context.__class__.__name__,
-                        getattr(form.getContent(), '__name__', 'noname').replace('++', ''),
-                        form.id),
-                    'widget_name': form.widgets['renderer'].name,
-                    'content': form.widgets['renderer'].render()
-                }
-            })
+            output.setdefault('events', []) \
+                  .append(get_json_widget_refresh_event(self.context, self.request,
+                                                        MilestoneParagraphInnerEditForm, 'renderer'))
         return output
 
 
@@ -192,9 +184,7 @@
 class MilestonesTable(ProtectedFormObjectMixin, BaseTable):
     """Milestones view inner table"""
 
-    @property
-    def id(self):
-        return 'milestones_{0}_list'.format(self.context.__name__)
+    prefix = 'milestones'
 
     hide_header = True
     sortOn = None
@@ -212,17 +202,11 @@
         attributes = super(MilestonesTable, self).data_attributes
         attributes['table'] = {
             'id': self.id,
-            'data-ams-plugins': 'pyams_content',
-            'data-ams-plugin-pyams_content-src': get_resource_path(pyams_content),
             'data-ams-location': absolute_url(IMilestoneContainer(self.context), self.request),
             'data-ams-tablednd-drag-handle': 'td.sorter',
-            'data-ams-tablednd-drop-target': 'set-milestones-order.json'
+            'data-ams-tablednd-drop-target': 'set-milestones-order.json',
+            'data-ams-visibility-switcher': 'switch-milestone-visibility.json'
         }
-        attributes.setdefault('tr', {}).update({
-            'id': lambda x, col: 'milestone_{0}::{1}'.format(get_parent(x, IMilestoneContainerTarget).__name__,
-                                                             x.__name__),
-            'data-ams-delete-target': 'delete-milestone.json'
-        })
         return attributes
 
     @reify
@@ -255,43 +239,15 @@
 
 @adapter_config(name='show-hide', context=(IMilestoneContainerTarget, IPyAMSLayer, MilestonesTable),
                 provides=IColumn)
-class MilestonesTableShowHideColumn(ProtectedFormObjectMixin, JsActionColumn):
+class MilestonesTableShowHideColumn(ProtectedFormObjectMixin, VisibilitySwitcherColumn):
     """Milestones container visibility switcher column"""
 
-    cssClasses = {'th': 'action',
-                  'td': 'action switcher'}
 
-    icon_class = 'fa fa-fw fa-eye'
-    icon_hint = _("Switch milestone visibility")
-
-    url = 'PyAMS_content.milestones.switchVisibility'
-
-    weight = 5
-
-    def get_icon(self, item):
-        if item.visible:
-            icon_class = 'fa fa-fw fa-eye'
-        else:
-            icon_class = 'fa fa-fw fa-eye-slash text-danger'
-        return '<i class="{icon_class}"></i>'.format(icon_class=icon_class)
-
-    def renderCell(self, item):
-        if self.permission and not self.request.has_permission(self.permission, context=item):
-            return self.get_icon(item)
-        else:
-            return super(MilestonesTableShowHideColumn, self).renderCell(item)
-
-
-@view_config(name='set-milestone-visibility.json', context=IMilestoneContainer, request_type=IPyAMSLayer,
+@view_config(name='switch-milestone-visibility.json', context=IMilestoneContainer, request_type=IPyAMSLayer,
              permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
-def set_milestone_visibility(request):
+def switch_milestone_visibility(request):
     """Set milestone visibility"""
-    container = IMilestoneContainer(request.context)
-    milestone = container.get(str(request.params.get('object_name')))
-    if milestone is None:
-        raise NotFound()
-    milestone.visible = not milestone.visible
-    return {'visible': milestone.visible}
+    return switch_element_visibility(request, IMilestoneContainer)
 
 
 @adapter_config(name='name', context=(IMilestoneContainerTarget, IPyAMSLayer, MilestonesTable), provides=IColumn)
@@ -343,50 +299,16 @@
     """Milestones table trash column"""
 
 
-@view_config(name='delete-milestone.json', context=IMilestoneContainer, request_type=IPyAMSLayer,
-             permission=MANAGE_PERMISSION, renderer='json', xhr=True)
-def delete_milestone(request):
-    """Delete milestone"""
-    translate = request.localizer.translate
-    name = request.params.get('object_name')
-    if not name:
-        return {
-            'status': 'message',
-            'messagebox': {
-                'status': 'error',
-                'content': translate(_("No provided object_name argument!"))
-            }
-        }
-    if name not in request.context:
-        return {
-            'status': 'message',
-            'messagebox': {
-                'status': 'error',
-                'content': translate(_("Given association name doesn't exist!"))
-            }
-        }
-    del request.context[name]
-    return {'status': 'success'}
-
-
 @adapter_config(name='milestones', context=(IMilestoneContainerTarget, IPyAMSLayer, IMilestonesParentForm),
                 provides=IInnerSubForm)
-@template_config(template='templates/milestones.pt', layer=IPyAMSLayer)
 @implementer(IMilestonesView)
-class MilestonesView(InnerAdminDisplayForm):
+class MilestonesView(InnerTableView):
     """Milestones view"""
 
-    fields = field.Fields(Interface)
-    weight = 100
+    title = _("Milestones")
 
-    def __init__(self, context, request, view):
-        super(MilestonesView, self).__init__(context, request, view)
-        self.table = MilestonesTable(context, request)
-        self.table.view = self
-
-    def update(self):
-        super(MilestonesView, self).update()
-        self.table.update()
+    table_class = MilestonesTable
+    weight = 100
 
 
 #
@@ -429,19 +351,10 @@
     """Milestone add form, JSON renderer"""
 
     def get_ajax_output(self, changes):
-        table = MilestonesTable(self.context, self.request)
-        table.update()
         return {
             'status': 'success',
             'message': self.request.localizer.translate(_("Milestone was correctly added")),
-            'events': [{
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'PyAMS_content.milestones.refreshMilestones',
-                    'object_id': table.id,
-                    'table': table.render()
-                }
-            }]
+            'events': [get_json_switched_table_refresh_event(self.context, self.request, MilestonesTable)]
         }
 
 
@@ -449,6 +362,8 @@
 class MilestonePropertiesEditForm(AdminDialogEditForm):
     """Milestone properties edit form"""
 
+    prefix = 'milestone_properties.'
+
     legend = _("Edit milestone properties")
     icon_css_class = 'fa fa-fw fa-arrows-h'
 
@@ -467,16 +382,6 @@
         updated = changes.get(IMilestone, ())
         if ('title' in updated) or ('anchor' in updated):
             target = get_parent(self.context, IMilestoneContainerTarget)
-            table = MilestonesTable(target, self.request)
-            table.update()
-            row = table.setUpRow(self.context)
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'MyAMS.skin.refreshRow',
-                    'object_id': 'milestone_{0}::{1}'.format(target.__name__,
-                                                             self.context.__name__),
-                    'row': table.renderRow(row)
-                }
-            })
+            output.setdefault('events', []).append(get_json_table_row_refresh_event(target, self.request,
+                                                                                    MilestonesTable, self.context))
         return output
--- a/src/pyams_content/component/paragraph/zmi/pictogram.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/component/paragraph/zmi/pictogram.py	Wed Apr 11 16:46:31 2018 +0200
@@ -29,7 +29,6 @@
 from pyams_i18n.interfaces import II18n
 from pyams_skin.interfaces.viewlet import IToolbarAddingMenu, IWidgetTitleViewletManager
 from pyams_skin.layer import IPyAMSLayer
-from pyams_utils.interfaces import MANAGE_PERMISSION
 from pyams_zmi.interfaces import IPropertiesEditForm
 from z3c.form.interfaces import INPUT_MODE
 from z3c.table.interfaces import IValues, IColumn
@@ -41,23 +40,23 @@
     BaseParagraphPropertiesEditForm, BaseParagraphAJAXEditForm
 from pyams_content.features.renderer.zmi import BaseRenderedContentRenderer
 from pyams_content.features.renderer.zmi.widget import RendererFieldWidget
-from pyams_content.skin import pyams_content
 from pyams_form.form import AJAXAddForm, AJAXEditForm
 from pyams_form.security import ProtectedFormObjectMixin
 from pyams_i18n.column import I18nAttrColumn
 from pyams_pagelet.pagelet import pagelet_config
-from pyams_skin.table import BaseTable, SorterColumn, JsActionColumn, I18nColumn, TrashColumn
+from pyams_skin.container import switch_element_visibility
+from pyams_skin.event import get_json_widget_refresh_event, get_json_switched_table_refresh_event, \
+    get_json_table_row_refresh_event
+from pyams_skin.table import BaseTable, SorterColumn, I18nColumn, TrashColumn, VisibilitySwitcherColumn
 from pyams_skin.viewlet.toolbar import ToolbarAction
-from pyams_template.template import template_config
 from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter
-from pyams_utils.fanstatic import get_resource_path
 from pyams_utils.text import get_text_start
 from pyams_utils.traversing import get_parent
 from pyams_utils.url import absolute_url
 from pyams_viewlet.viewlet import viewlet_config
-from pyams_zmi.form import AdminDialogAddForm, InnerAdminDisplayForm, AdminDialogEditForm
+from pyams_zmi.form import AdminDialogAddForm, AdminDialogEditForm
+from pyams_zmi.zmi.table import InnerTableView
 from pyramid.decorator import reify
-from pyramid.exceptions import NotFound
 from pyramid.view import view_config
 from z3c.form import field, button
 from z3c.table.column import GetAttrColumn
@@ -115,6 +114,8 @@
 class PictogramParagraphPropertiesEditForm(BaseParagraphPropertiesEditForm):
     """Pictogram paragraph properties edit form"""
 
+    prefix = 'pictograms_properties.'
+
     @property
     def title(self):
         content = get_parent(self.context, IWfSharedContent)
@@ -161,20 +162,8 @@
         output = super(PictogramParagraphInnerAJAXEditForm, self).get_ajax_output(changes)
         updated = changes.get(IPictogramParagraph, ())
         if 'renderer' in updated:
-            form = PictogramParagraphInnerEditForm(self.context, self.request)
-            form.update()
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'MyAMS.skin.refreshWidget',
-                    'parent_id': '{0}_{1}_{2}'.format(
-                        self.context.__class__.__name__,
-                        getattr(form.getContent(), '__name__', 'noname').replace('++', ''),
-                        form.id),
-                    'widget_name': form.widgets['renderer'].name,
-                    'content': form.widgets['renderer'].render()
-                }
-            })
+            output.setdefault('events', []).append(
+                get_json_widget_refresh_event(self.context, self.request, PictogramParagraphInnerEditForm, 'renderer'))
         return output
 
 
@@ -194,9 +183,7 @@
 class PictogramsTable(ProtectedFormObjectMixin, BaseTable):
     """Pictograms view inner table"""
 
-    @property
-    def id(self):
-        return 'pictograms_{0}_list'.format(self.context.__name__)
+    prefix = 'pictograms'
 
     hide_header = True
     sortOn = None
@@ -214,17 +201,11 @@
         attributes = super(PictogramsTable, self).data_attributes
         attributes['table'] = {
             'id': self.id,
-            'data-ams-plugins': 'pyams_content',
-            'data-ams-plugin-pyams_content-src': get_resource_path(pyams_content),
             'data-ams-location': absolute_url(IPictogramContainer(self.context), self.request),
             'data-ams-tablednd-drag-handle': 'td.sorter',
-            'data-ams-tablednd-drop-target': 'set-pictograms-order.json'
+            'data-ams-tablednd-drop-target': 'set-pictograms-order.json',
+            'data-ams-visibility-switcher': 'switch-pictogram-visibility.json'
         }
-        attributes.setdefault('tr', {}).update({
-            'id': lambda x, col: 'pictogram_{0}::{1}'.format(get_parent(x, IPictogramContainerTarget).__name__,
-                                                             x.__name__),
-            'data-ams-delete-target': 'delete-pictogram.json'
-        })
         return attributes
 
     @reify
@@ -276,43 +257,15 @@
 
 @adapter_config(name='show-hide', context=(IPictogramContainerTarget, IPyAMSLayer, PictogramsTable),
                 provides=IColumn)
-class PictogramsTableShowHideColumn(ProtectedFormObjectMixin, JsActionColumn):
+class PictogramsTableShowHideColumn(ProtectedFormObjectMixin, VisibilitySwitcherColumn):
     """Pictograms container visibility switcher column"""
 
-    cssClasses = {'th': 'action',
-                  'td': 'action switcher'}
 
-    icon_class = 'fa fa-fw fa-eye'
-    icon_hint = _("Switch pictogram visibility")
-
-    url = 'PyAMS_content.pictograms.switchVisibility'
-
-    weight = 5
-
-    def get_icon(self, item):
-        if item.visible:
-            icon_class = 'fa fa-fw fa-eye'
-        else:
-            icon_class = 'fa fa-fw fa-eye-slash text-danger'
-        return '<i class="{icon_class}"></i>'.format(icon_class=icon_class)
-
-    def renderCell(self, item):
-        if self.permission and not self.request.has_permission(self.permission, context=item):
-            return self.get_icon(item)
-        else:
-            return super(PictogramsTableShowHideColumn, self).renderCell(item)
-
-
-@view_config(name='set-pictogram-visibility.json', context=IPictogramContainer, request_type=IPyAMSLayer,
+@view_config(name='switch-pictogram-visibility.json', context=IPictogramContainer, request_type=IPyAMSLayer,
              permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
-def set_pictogram_visibility(request):
+def switch_pictogram_visibility(request):
     """Set pictogram visibility"""
-    container = IPictogramContainer(request.context)
-    pictogram = container.get(str(request.params.get('object_name')))
-    if pictogram is None:
-        raise NotFound()
-    pictogram.visible = not pictogram.visible
-    return {'visible': pictogram.visible}
+    return switch_element_visibility(request, IPictogramContainer)
 
 
 @adapter_config(name='name', context=(IPictogramContainerTarget, IPyAMSLayer, PictogramsTable), provides=IColumn)
@@ -350,50 +303,16 @@
     """Pictograms table trash column"""
 
 
-@view_config(name='delete-pictogram.json', context=IPictogramContainer, request_type=IPyAMSLayer,
-             permission=MANAGE_PERMISSION, renderer='json', xhr=True)
-def delete_pictogram(request):
-    """Delete pictogram"""
-    translate = request.localizer.translate
-    name = request.params.get('object_name')
-    if not name:
-        return {
-            'status': 'message',
-            'messagebox': {
-                'status': 'error',
-                'content': translate(_("No provided object_name argument!"))
-            }
-        }
-    if name not in request.context:
-        return {
-            'status': 'message',
-            'messagebox': {
-                'status': 'error',
-                'content': translate(_("Given pictogram name doesn't exist!"))
-            }
-        }
-    del request.context[name]
-    return {'status': 'success'}
-
-
 @adapter_config(name='pictograms', context=(IPictogramContainerTarget, IPyAMSLayer, IPictogramsParentForm),
                 provides=IInnerSubForm)
-@template_config(template='templates/pictograms.pt', layer=IPyAMSLayer)
 @implementer(IPictogramsView)
-class PictogramsView(InnerAdminDisplayForm):
+class PictogramsView(InnerTableView):
     """Pictograms view"""
 
-    fields = field.Fields(Interface)
-    weight = 100
+    title = _("Pictograms")
 
-    def __init__(self, context, request, view):
-        super(PictogramsView, self).__init__(context, request, view)
-        self.table = PictogramsTable(context, request)
-        self.table.view = self
-
-    def update(self):
-        super(PictogramsView, self).update()
-        self.table.update()
+    table_class = PictogramsTable
+    weight = 100
 
 
 #
@@ -441,19 +360,10 @@
     """Pictogram add form, JSON renderer"""
 
     def get_ajax_output(self, changes):
-        table = PictogramsTable(self.context, self.request)
-        table.update()
         return {
             'status': 'success',
             'message': self.request.localizer.translate(_("Pictogram was correctly added")),
-            'events': [{
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'PyAMS_content.pictograms.refreshPictograms',
-                    'object_id': table.id,
-                    'table': table.render()
-                }
-            }]
+            'events': [get_json_switched_table_refresh_event(self.context, self.request, PictogramsTable), ]
         }
 
 
@@ -461,6 +371,8 @@
 class PictogramPropertiesEditForm(AdminDialogEditForm):
     """Pictogram properties edit form"""
 
+    prefix = 'pictogram_properties.'
+
     legend = _("Edit pictogram properties")
     icon_css_class = 'fa fa-fw fa-linode'
 
@@ -484,16 +396,6 @@
         updated = changes.get(IPictogramItem, ())
         if updated:
             target = get_parent(self.context, IPictogramContainerTarget)
-            table = PictogramsTable(target, self.request)
-            table.update()
-            row = table.setUpRow(self.context)
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'MyAMS.skin.refreshRow',
-                    'object_id': 'pictogram_{0}::{1}'.format(target.__name__,
-                                                             self.context.__name__),
-                    'row': table.renderRow(row)
-                }
-            })
+            output.setdefault('events', []).append(
+                get_json_table_row_refresh_event(target, self.request, PictogramsTable, self.context))
         return output
--- a/src/pyams_content/component/paragraph/zmi/templates/milestones.pt	Wed Apr 11 16:44:46 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,14 +0,0 @@
-<div class="form-group" i18n:domain="pyams_content">
-	<fieldset class="margin-top-10 padding-top-5 padding-bottom-0">
-		<legend
-			class="inner switcher margin-bottom-5 padding-right-10 no-y-padding pull-left width-auto"
-			tal:attributes="data-ams-switcher-state 'open' if view.table.values else None">
-			<i18n:var translate="">Milestones</i18n:var>
-		</legend>
-		<div class="pull-left persistent">
-			<tal:var content="structure provider:pyams.widget_title" />
-		</div>
-		<div class="clearfix"></div>
-		<tal:var content="structure view.table.render()" />
-	</fieldset>
-</div>
--- a/src/pyams_content/component/paragraph/zmi/templates/paragraphs.pt	Wed Apr 11 16:44:46 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,14 +0,0 @@
-<div class="form-group" i18n:domain="pyams_content">
-	<fieldset class="margin-top-10 padding-top-5 padding-bottom-0">
-		<legend
-			class="inner switcher margin-bottom-5 padding-right-10 no-y-padding pull-left width-auto"
-			data-ams-switcher-state="open">
-			<i18n:var translate="">Paragraphs</i18n:var>
-		</legend>
-		<div class="pull-left persistent">
-			<tal:var content="structure provider:pyams.widget_title" />
-		</div>
-		<div class="clearfix"></div>
-		<tal:var content="structure view.table.render()" />
-	</fieldset>
-</div>
--- a/src/pyams_content/component/paragraph/zmi/templates/pictograms.pt	Wed Apr 11 16:44:46 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,14 +0,0 @@
-<div class="form-group" i18n:domain="pyams_content">
-	<fieldset class="margin-top-10 padding-top-5 padding-bottom-0">
-		<legend
-			class="inner switcher margin-bottom-5 padding-right-10 no-y-padding pull-left width-auto"
-			tal:attributes="data-ams-switcher-state 'open' if view.table.values else None">
-			<i18n:var translate="">Pictograms</i18n:var>
-		</legend>
-		<div class="pull-left persistent">
-			<tal:var content="structure provider:pyams.widget_title" />
-		</div>
-		<div class="clearfix"></div>
-		<tal:var content="structure view.table.render()" />
-	</fieldset>
-</div>
--- a/src/pyams_content/component/paragraph/zmi/verbatim.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/component/paragraph/zmi/verbatim.py	Wed Apr 11 16:46:31 2018 +0200
@@ -9,6 +9,7 @@
 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
 # FOR A PARTICULAR PURPOSE.
 #
+from pyams_skin.event import get_json_widget_refresh_event
 
 __docformat__ = 'restructuredtext'
 
@@ -95,6 +96,8 @@
 class VerbatimParagraphPropertiesEditForm(BaseParagraphPropertiesEditForm):
     """Verbatim paragraph properties edit form"""
 
+    prefix = 'verbatim_properties.'
+
     legend = _("Edit verbatim paragraph properties")
     icon_css_class = 'fa fa-fw fa-quote-right'
 
@@ -141,20 +144,8 @@
         output = super(VerbatimParagraphInnerAJAXEditForm, self).get_ajax_output(changes)
         updated = changes.get(IVerbatimParagraph, ())
         if 'renderer' in updated:
-            form = VerbatimParagraphInnerEditForm(self.context, self.request)
-            form.update()
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'MyAMS.skin.refreshWidget',
-                    'parent_id': '{0}_{1}_{2}'.format(
-                        self.context.__class__.__name__,
-                        getattr(form.getContent(), '__name__', 'noname').replace('++', ''),
-                        form.id),
-                    'widget_name': form.widgets['renderer'].name,
-                    'content': form.widgets['renderer'].render()
-                }
-            })
+            output.setdefault('events', []).append(
+                get_json_widget_refresh_event(self.context, self.request, VerbatimParagraphInnerEditForm, 'renderer'))
         return output
 
 
--- a/src/pyams_content/component/paragraph/zmi/video.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/component/paragraph/zmi/video.py	Wed Apr 11 16:46:31 2018 +0200
@@ -38,6 +38,7 @@
 from pyams_content.features.renderer.zmi.widget import RendererFieldWidget
 from pyams_form.group import NamedWidgetsGroup
 from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.event import get_json_form_refresh_event, get_json_widget_refresh_event
 from pyams_utils.adapter import adapter_config
 from pyams_viewlet.viewlet import viewlet_config
 from pyams_zmi.form import AdminDialogAddForm
@@ -106,6 +107,8 @@
 class VideoParagraphPropertiesEditForm(BaseParagraphPropertiesEditForm):
     """Video paragraph properties edit form"""
 
+    prefix = 'video_properties.'
+
     legend = _("Edit video properties")
     dialog_class = 'modal-large'
     icon_css_class = 'fa fa-fw fa-film'
@@ -188,33 +191,12 @@
         if 'data' in updated:
             # we have to commit transaction to be able to handle blobs...
             ITransactionManager(self.context).get().commit()
-            form = VideoParagraphPropertiesInnerEditForm(self.context, self.request)
-            form.update()
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'object_id': '{0}_{1}_{2}'.format(
-                        self.context.__class__.__name__,
-                        getattr(form.getContent(), '__name__', 'noname').replace('++', ''),
-                        form.id),
-                    'content': form.render()
-                }
-            })
+            output.setdefault('events', []).append(
+                get_json_form_refresh_event(self.context, self.request, VideoParagraphPropertiesInnerEditForm))
         elif 'renderer' in updated:
-            form = VideoParagraphPropertiesInnerEditForm(self.context, self.request)
-            form.update()
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'MyAMS.skin.refreshWidget',
-                    'parent_id': '{0}_{1}_{2}'.format(
-                        self.context.__class__.__name__,
-                        getattr(form.getContent(), '__name__', 'noname').replace('++', ''),
-                        form.id),
-                    'widget_name': form.widgets['renderer'].name,
-                    'content': form.widgets['renderer'].render()
-                }
-            })
+            output.setdefault('events', []).append(
+                get_json_widget_refresh_event(self.context, self.request,
+                                              VideoParagraphPropertiesInnerEditForm, 'renderer'))
         return output
 
 
--- a/src/pyams_content/component/theme/zmi/manager.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/component/theme/zmi/manager.py	Wed Apr 11 16:46:31 2018 +0200
@@ -19,15 +19,16 @@
 from pyams_content.component.theme.interfaces import IThemesManagerTarget, IThemesManager
 from pyams_content.interfaces import MANAGE_TOOL_PERMISSION
 from pyams_skin.layer import IPyAMSLayer
-from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
 from pyams_utils.interfaces.data import IObjectData
 from pyams_zmi.interfaces.menu import IPropertiesMenu
 from pyams_zmi.layer import IAdminLayer
 
 # import packages
+from pyams_content.skin import pyams_content
 from pyams_form.form import AJAXEditForm
 from pyams_pagelet.pagelet import pagelet_config
 from pyams_skin.viewlet.menu import MenuItem
+from pyams_utils.fanstatic import get_resource_path
 from pyams_viewlet.viewlet import viewlet_config
 from pyams_zmi.form import AdminDialogEditForm
 from pyramid.view import view_config
@@ -53,6 +54,8 @@
 class ThemesManagerEditForm(AdminDialogEditForm):
     """Themes manager edit form"""
 
+    prefix = 'manager_themes.'
+
     legend = _("Selected themes")
 
     fields = field.Fields(IThemesManager)
@@ -63,11 +66,12 @@
         super(ThemesManagerEditForm, self).updateWidgets(prefix)
         if 'extract_name' in self.widgets:
             widget = self.widgets['extract_name']
-            widget.object_data = {'ams-plugins': 'pyams_content',
-                                  'ams-plugin-pyams_content-src':
-                                      '/--static--/pyams_content/js/pyams_content{MyAMS.devext}.js',
-                                  'ams-plugin-pyams_content-callback': 'PyAMS_content.themes.initExtracts',
-                                  'ams-plugin-pyams_content-async': 'false'}
+            widget.object_data = {
+                'ams-plugins': 'pyams_content',
+                'ams-plugin-pyams_content-src': get_resource_path(pyams_content),
+                'ams-plugin-pyams_content-callback': 'PyAMS_content.themes.initExtracts',
+                'ams-plugin-pyams_content-async': 'false'
+            }
             alsoProvides(widget, IObjectData)
 
 
--- a/src/pyams_content/component/theme/zmi/portlet.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/component/theme/zmi/portlet.py	Wed Apr 11 16:46:31 2018 +0200
@@ -35,7 +35,8 @@
 class PortletSettingsThemesEditor(InnerAdminEditForm):
     """Portlet settings for themes"""
 
-    id = 'themes_form'
+    prefix = 'themes_form.'
+
     tab_label = _("Themes")
     legend = None
 
--- a/src/pyams_content/component/video/zmi/paragraph.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/component/video/zmi/paragraph.py	Wed Apr 11 16:46:31 2018 +0200
@@ -31,7 +31,7 @@
 
 # import packages
 from pyams_content.component.paragraph.zmi import BaseParagraphAddMenu, BaseParagraphAJAXAddForm, \
-    BaseParagraphPropertiesEditForm, BaseParagraphAJAXEditForm
+    BaseParagraphPropertiesEditForm, BaseParagraphAJAXEditForm, get_json_paragraph_refresh_event
 from pyams_content.component.video.paragraph import ExternalVideoParagraph
 from pyams_form.group import NamedWidgetsGroup
 from pyams_pagelet.pagelet import pagelet_config
@@ -179,6 +179,8 @@
 class ExternalVideoParagraphPropertiesEditForm(BaseParagraphPropertiesEditForm):
     """External video paragraph properties edit form"""
 
+    prefix = 'externalvideo_properties.'
+
     legend = _("Edit video properties")
     icon_css_class = 'fa fa-fw fa-youtube-play'
 
@@ -232,15 +234,8 @@
     def get_ajax_output(self, changes):
         output = super(ExternalVideoParagraphPropertiesAJAXEditForm, self).get_ajax_output(changes)
         if 'title' in changes.get(IBaseParagraph, ()):
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'PyAMS_content.paragraphs.refreshParagraph',
-                    'object_name': self.context.__name__,
-                    'title': II18n(self.context).query_attribute('title', request=self.request),
-                    'visible': self.context.visible
-                }
-            })
+            output.setdefault('events', []).append(
+                get_json_paragraph_refresh_event(self.context, self.request))
         return output
 
 
--- a/src/pyams_content/features/alert/__init__.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/features/alert/__init__.py	Wed Apr 11 16:46:31 2018 +0200
@@ -17,7 +17,7 @@
 from persistent import Persistent
 
 # import interfaces
-from pyams_content.features.alert.interfaces import IAlertItem, IAlertTarget
+from pyams_content.features.alert.interfaces import IAlertItem
 from pyams_content.interfaces import MANAGE_SITE_ROOT_PERMISSION
 from pyams_content.reference.pictograms import IPictogramTable
 from pyams_form.interfaces.form import IFormContextPermissionChecker
--- a/src/pyams_content/features/alert/zmi/__init__.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/features/alert/zmi/__init__.py	Wed Apr 11 16:46:31 2018 +0200
@@ -26,6 +26,7 @@
 from pyams_content.features.alert.zmi.container import AlertContainerView, AlertContainerTable
 from pyams_form.form import AJAXAddForm, AJAXEditForm
 from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.event import get_json_table_row_refresh_event
 from pyams_skin.viewlet.toolbar import ToolbarAction
 from pyams_utils.traversing import get_parent
 from pyams_utils.url import absolute_url
@@ -79,6 +80,8 @@
 class AlertItemPropertiesEditForm(AdminDialogEditForm):
     """Alert item properties edit form"""
 
+    prefix = 'alert_properties.'
+
     legend = _("Edit alert properties")
     icon_css_class = 'fa fa-fw fa-exclamation-triangle'
 
@@ -97,15 +100,6 @@
         updated = changes.get(IAlertItem, ())
         if updated:
             target = get_parent(self.context, IAlertTarget)
-            table = AlertContainerTable(target, self.request)
-            table.update()
-            row = table.setUpRow(self.context)
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'MyAMS.skin.refreshRow',
-                    'object_id': 'alert_{0}'.format(self.context.__name__),
-                    'row': table.renderRow(row)
-                }
-            })
+            output.setdefault('events', []).append(
+                get_json_table_row_refresh_event(target, self.request, AlertContainerTable, self.context))
         return output
--- a/src/pyams_content/features/alert/zmi/container.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/features/alert/zmi/container.py	Wed Apr 11 16:46:31 2018 +0200
@@ -31,7 +31,7 @@
 from pyams_i18n.column import I18nAttrColumn
 from pyams_pagelet.pagelet import pagelet_config
 from pyams_skin.page import DefaultPageHeaderAdapter
-from pyams_skin.table import BaseTable, SorterColumn, JsActionColumn, I18nColumn, TrashColumn
+from pyams_skin.table import BaseTable, SorterColumn, I18nColumn, TrashColumn, VisibilitySwitcherColumn
 from pyams_skin.viewlet.menu import MenuItem
 from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter
 from pyams_utils.fanstatic import get_resource_path
@@ -60,7 +60,8 @@
 class AlertContainerTable(BaseTable):
     """Alerts container table"""
 
-    id = 'alerts_table'
+    prefix = 'alerts'
+
     hide_header = True
     sortOn = None
 
@@ -75,11 +76,8 @@
             'data-ams-plugin-pyams_content-src': get_resource_path(pyams_content),
             'data-ams-location': absolute_url(IAlertContainer(self.context), self.request),
             'data-ams-tablednd-drag-handle': 'td.sorter',
-            'data-ams-tablednd-drop-target': 'set-alerts-order.json'
-        })
-        attributes.setdefault('tr', {}).update({
-            'id': lambda x, col: 'alert_{0}'.format(x.__name__),
-            'data-ams-delete-target': 'delete-alert.json'
+            'data-ams-tablednd-drop-target': 'set-alerts-order.json',
+            'data-ams-visibility-switcher': 'switch-alert-visibility.json'
         })
         return attributes
 
@@ -118,30 +116,14 @@
 
 
 @adapter_config(name='show-hide', context=(IAlertTarget, IPyAMSLayer, AlertContainerTable), provides=IColumn)
-class AlertContainerShowHideColumn(JsActionColumn):
+class AlertContainerShowHideColumn(VisibilitySwitcherColumn):
     """Alert container visibility switcher column"""
 
-    cssClasses = {'th': 'action',
-                  'td': 'action switcher'}
 
-    icon_class = 'fa fa-fw fa-eye'
-    icon_hint = _("Switch alert visibility")
-
-    url = 'PyAMS_content.alerts.switchVisibility'
-
-    weight = 5
-
-    def get_icon_class(self, item):
-        if item.visible:
-            return self.icon_class
-        else:
-            return 'fa fa-fw fa-eye-slash text-danger'
-
-
-@view_config(name='set-alert-visibility.json', context=IAlertContainer, request_type=IPyAMSLayer,
+@view_config(name='switch-alert-visibility.json', context=IAlertContainer, request_type=IPyAMSLayer,
              permission=MANAGE_SITE_ROOT_PERMISSION, renderer='json', xhr=True)
-def set_alert_visibility(request):
-    """Set alert visibility"""
+def switch_alert_visibility(request):
+    """Switch alert visibility"""
     container = IAlertContainer(request.context)
     alert = container.get(str(request.params.get('object_name')))
     if alert is None:
@@ -200,31 +182,7 @@
 class AlertContainerTrashColumn(TrashColumn):
     """Alert container trash column"""
 
-
-@view_config(name='delete-alert.json', context=IAlertTarget, request_type=IPyAMSLayer,
-             permission=MANAGE_SITE_ROOT_PERMISSION, renderer='json', xhr=True)
-def delete_alert(request):
-    """Delete alert"""
-    translate = request.localizer.translate
-    name = request.params.get('object_name')
-    if not name:
-        return {
-            'status': 'message',
-            'messagebox': {
-                'status': 'error',
-                'content': translate(_("No provided object_name argument!"))
-            }
-        }
-    if name not in request.context:
-        return {
-            'status': 'message',
-            'messagebox': {
-                'status': 'error',
-                'content': translate(_("Given alert name doesn't exist!"))
-            }
-        }
-    del request.context[name]
-    return {'status': 'success'}
+    permission = MANAGE_SITE_ROOT_PERMISSION
 
 
 @pagelet_config(name='alerts.html', context=IAlertTarget, layer=IPyAMSLayer, permission=MANAGE_SITE_ROOT_PERMISSION)
--- a/src/pyams_content/features/renderer/zmi/__init__.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/features/renderer/zmi/__init__.py	Wed Apr 11 16:46:31 2018 +0200
@@ -100,6 +100,8 @@
 class RendererPropertiesEditForm(AdminDialogEditForm):
     """Renderer properties edit form"""
 
+    prefix = 'renderer_properties.'
+
     legend = _("Edit renderer properties")
     icon_css_class = 'fa fa-fw fa-pencil-square-o'
 
--- a/src/pyams_content/features/review/zmi/__init__.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/features/review/zmi/__init__.py	Wed Apr 11 16:46:31 2018 +0200
@@ -136,24 +136,28 @@
     def get_ajax_output(self, changes):
         translate = self.request.localizer.translate
         if changes:
-            return {'status': 'success',
-                    'message': translate(_("Request successful. "
-                                           "{count} new notification(s) have been sent")).format(count=changes),
-                    'events': [{
-                        'event': 'myams.refresh',
-                        'options': {
-                            'handler': 'PyAMS_content.review.updateComments'
-                        }
-                    }]}
+            return {
+                'status': 'success',
+                'message': translate(_("Request successful. "
+                                       "{count} new notification(s) have been sent")).format(count=changes),
+                'events': [{
+                    'event': 'myams.refresh',
+                    'options': {
+                        'handler': 'PyAMS_content.review.updateComments'
+                    }
+                }]
+            }
         else:
-            return {'status': 'info',
-                    'message': translate(_("Request successful. No new notification have been sent")),
-                    'events': [{
-                        'event': 'myams.refresh',
-                        'options': {
-                            'handler': 'PyAMS_content.review.updateComments'
-                        }
-                    }]}
+            return {
+                'status': 'info',
+                'message': translate(_("Request successful. No new notification have been sent")),
+                'events': [{
+                    'event': 'myams.refresh',
+                    'options': {
+                        'handler': 'PyAMS_content.review.updateComments'
+                    }
+                }]
+            }
 
 
 #
@@ -257,8 +261,10 @@
         translate = request.localizer.translate
         comment_body = request.params.get('comment')
         if not comment_body:
-            return {'status': 'error',
-                    'message': translate(_("Message is mandatory!"))}
+            return {
+                'status': 'error',
+                'message': translate(_("Message is mandatory!"))
+            }
         # add new comment
         comments = IReviewComments(request.context)
         comment = ReviewComment(owner=request.principal.id,
@@ -274,7 +280,11 @@
                                      translate=query_utility(IChameleonTranslate),
                                      options={'comment': comment,
                                               'profile': profile})
-        return {'status': 'success',
-                'callback': 'PyAMS_content.review.addCommentCallback',
-                'options': {'content': comment_body,
-                            'count': len(comments)}}
+        return {
+            'status': 'success',
+            'callback': 'PyAMS_content.review.addCommentCallback',
+            'options': {
+                'content': comment_body,
+                'count': len(comments)
+            }
+        }
Binary file src/pyams_content/locales/fr/LC_MESSAGES/pyams_content.mo has changed
--- a/src/pyams_content/locales/fr/LC_MESSAGES/pyams_content.po	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/locales/fr/LC_MESSAGES/pyams_content.po	Wed Apr 11 16:46:31 2018 +0200
@@ -5,7 +5,7 @@
 msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE 1.0\n"
-"POT-Creation-Date: 2018-04-04 15:32+0200\n"
+"POT-Creation-Date: 2018-04-06 13:47+0200\n"
 "PO-Revision-Date: 2015-09-10 10:42+0200\n"
 "Last-Translator: Thierry Florac <tflorac@ulthar.net>\n"
 "Language-Team: French\n"
@@ -108,9 +108,7 @@
 msgstr "Contenu audio associé"
 
 #: src/pyams_content/component/gallery/zmi/file.py:258
-#: src/pyams_content/component/paragraph/zmi/milestone.py:357
-#: src/pyams_content/component/paragraph/zmi/pictogram.py:363
-#: src/pyams_content/component/association/zmi/__init__.py:288
+#: src/pyams_content/component/association/zmi/__init__.py:268
 #: src/pyams_content/shared/common/zmi/types.py:208
 #: src/pyams_content/shared/common/zmi/types.py:457
 #: src/pyams_content/shared/imagemap/zmi/container.py:169
@@ -204,6 +202,7 @@
 #: src/pyams_content/component/gallery/interfaces/__init__.py:50
 #: src/pyams_content/component/extfile/interfaces/__init__.py:77
 #: src/pyams_content/component/illustration/interfaces/__init__.py:44
+#: src/pyams_content/reference/pictograms/interfaces/__init__.py:48
 msgid "Accessibility title"
 msgstr "Alternative (accessibilité)"
 
@@ -289,8 +288,8 @@
 
 #: src/pyams_content/component/gallery/interfaces/__init__.py:94
 #: src/pyams_content/component/extfile/interfaces/__init__.py:36
-#: src/pyams_content/component/paragraph/zmi/milestone.py:301
-#: src/pyams_content/component/paragraph/zmi/container.py:225
+#: src/pyams_content/component/paragraph/zmi/milestone.py:269
+#: src/pyams_content/component/paragraph/zmi/container.py:223
 #: src/pyams_content/component/paragraph/interfaces/milestone.py:46
 #: src/pyams_content/component/links/zmi/reverse.py:71
 #: src/pyams_content/shared/common/zmi/dashboard.py:109
@@ -299,7 +298,7 @@
 #: src/pyams_content/shared/site/zmi/folder.py:64
 #: src/pyams_content/root/zmi/templates/advanced-search.pt:188
 #: src/pyams_content/interfaces/__init__.py:99
-#: src/pyams_content/reference/pictograms/zmi/__init__.py:171
+#: src/pyams_content/reference/pictograms/zmi/__init__.py:172
 msgid "Title"
 msgstr "Titre"
 
@@ -468,6 +467,7 @@
 
 #: src/pyams_content/component/extfile/interfaces/__init__.py:78
 #: src/pyams_content/component/illustration/interfaces/__init__.py:45
+#: src/pyams_content/reference/pictograms/interfaces/__init__.py:49
 msgid "Alternate title used to describe image content"
 msgstr ""
 "Ce texte est affiché lorsque le contenu ne peut être téléchargé ou affiché ; "
@@ -543,7 +543,7 @@
 
 #: src/pyams_content/component/paragraph/milestone.py:206
 #: src/pyams_content/component/paragraph/milestone.py:228
-#: src/pyams_content/component/paragraph/zmi/templates/milestones.pt:6
+#: src/pyams_content/component/paragraph/zmi/milestone.py:320
 msgid "Milestones"
 msgstr "Chronologie"
 
@@ -569,7 +569,7 @@
 msgstr "Points clés"
 
 #: src/pyams_content/component/paragraph/container.py:82
-#: src/pyams_content/component/paragraph/zmi/templates/paragraphs.pt:6
+#: src/pyams_content/component/paragraph/zmi/container.py:307
 msgid "Paragraphs"
 msgstr "Paragraphes"
 
@@ -579,7 +579,7 @@
 
 #: src/pyams_content/component/paragraph/pictogram.py:197
 #: src/pyams_content/component/paragraph/pictogram.py:219
-#: src/pyams_content/component/paragraph/zmi/templates/pictograms.pt:6
+#: src/pyams_content/component/paragraph/zmi/pictogram.py:334
 msgid "Pictograms"
 msgstr "Pictogrammes"
 
@@ -591,6 +591,16 @@
 msgid "Selected pictogram is missing"
 msgstr "le pictogramme sélectionné est introuvable"
 
+#: src/pyams_content/component/paragraph/keynumber.py:190
+#: src/pyams_content/component/paragraph/keynumber.py:213
+#: src/pyams_content/component/paragraph/zmi/keynumber.py:302
+msgid "Key numbers"
+msgstr "Chiffres-clés"
+
+#: src/pyams_content/component/paragraph/keynumber.py:222
+msgid "Key numbers paragraph"
+msgstr "Chiffres-clés"
+
 #: src/pyams_content/component/paragraph/frame.py:52
 msgid "Framed text"
 msgstr "Encadré"
@@ -639,53 +649,44 @@
 msgid "Header paragraph"
 msgstr "Chapô"
 
-#: src/pyams_content/component/paragraph/zmi/milestone.py:80
+#: src/pyams_content/component/paragraph/zmi/milestone.py:77
 msgid "Milestones..."
 msgstr "Chronologie"
 
-#: src/pyams_content/component/paragraph/zmi/milestone.py:91
+#: src/pyams_content/component/paragraph/zmi/milestone.py:88
 msgid "Add new milestone paragraph"
 msgstr "Ajout d'une chronologie"
 
-#: src/pyams_content/component/paragraph/zmi/milestone.py:121
+#: src/pyams_content/component/paragraph/zmi/milestone.py:118
 msgid "Edit milestone paragraph properties"
 msgstr "Propriétés de la chronologie"
 
-#: src/pyams_content/component/paragraph/zmi/milestone.py:265
-msgid "Switch milestone visibility"
-msgstr "Cliquez pour rendre le jalon visible ou non"
-
-#: src/pyams_content/component/paragraph/zmi/milestone.py:310
+#: src/pyams_content/component/paragraph/zmi/milestone.py:278
 #: src/pyams_content/component/paragraph/interfaces/milestone.py:50
 msgid "Associated label"
 msgstr "Information associée"
 
-#: src/pyams_content/component/paragraph/zmi/milestone.py:319
+#: src/pyams_content/component/paragraph/zmi/milestone.py:287
 #: src/pyams_content/component/paragraph/interfaces/milestone.py:54
 msgid "Anchor"
 msgstr "Ancre"
 
-#: src/pyams_content/component/paragraph/zmi/milestone.py:401
+#: src/pyams_content/component/paragraph/zmi/milestone.py:335
 msgid "Add milestone"
 msgstr "Ajouter un jalon"
 
-#: src/pyams_content/component/paragraph/zmi/milestone.py:412
+#: src/pyams_content/component/paragraph/zmi/milestone.py:346
 msgid "Add new milestone"
 msgstr "Ajout d'un jalon"
 
-#: src/pyams_content/component/paragraph/zmi/milestone.py:452
+#: src/pyams_content/component/paragraph/zmi/milestone.py:386
 msgid "Edit milestone properties"
 msgstr "Propriétés du jalon"
 
-#: src/pyams_content/component/paragraph/zmi/milestone.py:436
+#: src/pyams_content/component/paragraph/zmi/milestone.py:370
 msgid "Milestone was correctly added"
 msgstr "Le jalon a été ajouté."
 
-#: src/pyams_content/component/paragraph/zmi/milestone.py:365
-#: src/pyams_content/component/association/zmi/__init__.py:292
-msgid "Given association name doesn't exist!"
-msgstr "Le nom d'association indiqué n'existe pas !"
-
 #: src/pyams_content/component/paragraph/zmi/keypoint.py:52
 msgid "Key points..."
 msgstr "Points clés"
@@ -749,103 +750,128 @@
 msgid "HTML content"
 msgstr "Contenu HTML"
 
-#: src/pyams_content/component/paragraph/zmi/container.py:71
+#: src/pyams_content/component/paragraph/zmi/container.py:74
 msgid "Paragraphs..."
 msgstr "Paragraphes"
 
-#: src/pyams_content/component/paragraph/zmi/container.py:163
-msgid "Switch paragraph visibility"
-msgstr "Cliquez pour rendre le paragraphe visible ou non"
-
-#: src/pyams_content/component/paragraph/zmi/container.py:241
+#: src/pyams_content/component/paragraph/zmi/container.py:239
 msgid "Show/hide all paragraphs"
 msgstr "Afficher/masquer tous les paragraphes"
 
-#: src/pyams_content/component/paragraph/zmi/container.py:287
-#: src/pyams_content/component/paragraph/zmi/container.py:296
+#: src/pyams_content/component/paragraph/zmi/container.py:285
+#: src/pyams_content/component/paragraph/zmi/container.py:294
 msgid "Paragraphs list"
 msgstr "Liste des paragraphes"
 
-#: src/pyams_content/component/paragraph/zmi/container.py:395
+#: src/pyams_content/component/paragraph/zmi/container.py:364
 #: src/pyams_content/component/association/zmi/paragraph.py:55
-#: src/pyams_content/component/association/zmi/__init__.py:104
+#: src/pyams_content/component/association/zmi/__init__.py:107
 msgid "Associations..."
 msgstr "Associations"
 
-#: src/pyams_content/component/paragraph/zmi/container.py:407
+#: src/pyams_content/component/paragraph/zmi/container.py:376
 msgid "Paragraphs associations"
 msgstr "Associations par paragraphe"
 
-#: src/pyams_content/component/paragraph/zmi/container.py:122
+#: src/pyams_content/component/paragraph/zmi/container.py:126
 msgid "No currently defined paragraph."
 msgstr "Aucun paragraphe associé à ce contenu."
 
-#: src/pyams_content/component/paragraph/zmi/container.py:250
+#: src/pyams_content/component/paragraph/zmi/container.py:248
 msgid "Click to open/close all paragraphs editors"
 msgstr "Afficher/masquer tous les paragraphes"
 
-#: src/pyams_content/component/paragraph/zmi/container.py:263
+#: src/pyams_content/component/paragraph/zmi/container.py:261
 msgid "Click to open/close paragraph editor"
 msgstr "Afficher/masquer ce paragraphe"
 
-#: src/pyams_content/component/paragraph/zmi/container.py:128
+#: src/pyams_content/component/paragraph/zmi/container.py:132
 msgid "Check allowed paragraph types to be able to create new paragraphs."
 msgstr ""
 "Vérifiez le paramétrage des types de paragraphes autorisés pour ajouter de "
 "nouveaux paragraphes."
 
-#: src/pyams_content/component/paragraph/zmi/pictogram.py:81
+#: src/pyams_content/component/paragraph/zmi/pictogram.py:84
 msgid "Pictograms..."
 msgstr "Pictogrammes"
 
-#: src/pyams_content/component/paragraph/zmi/pictogram.py:92
+#: src/pyams_content/component/paragraph/zmi/pictogram.py:95
 msgid "Add new pictogram paragraph"
 msgstr "Ajout de pictogrammes"
 
-#: src/pyams_content/component/paragraph/zmi/pictogram.py:122
+#: src/pyams_content/component/paragraph/zmi/pictogram.py:125
 msgid "Edit pictogram paragraph properties"
 msgstr "Propriétés des pictogrammes"
 
-#: src/pyams_content/component/paragraph/zmi/pictogram.py:285
-msgid "Switch pictogram visibility"
-msgstr "Cliquez pour rendre le paragraphe visible ou non"
-
-#: src/pyams_content/component/paragraph/zmi/pictogram.py:321
-#: src/pyams_content/shared/common/interfaces/types.py:39
-#: src/pyams_content/shared/form/zmi/field.py:171
-#: src/pyams_content/shared/form/interfaces/__init__.py:61
-msgid "Label"
-msgstr "Libellé"
-
-#: src/pyams_content/component/paragraph/zmi/pictogram.py:336
-#: src/pyams_content/component/paragraph/interfaces/video.py:41
-#: src/pyams_content/component/paragraph/interfaces/html.py:53
-#: src/pyams_content/component/video/interfaces/__init__.py:71
-msgid "Body"
-msgstr "Contenu HTML"
-
-#: src/pyams_content/component/paragraph/zmi/pictogram.py:407
+#. Default: Header
+#: src/pyams_content/component/paragraph/zmi/pictogram.py:297
+msgid "pictogram-item-header"
+msgstr "En-tête"
+
+#: src/pyams_content/component/paragraph/zmi/pictogram.py:312
+#: src/pyams_content/component/paragraph/zmi/keynumber.py:286
+#: src/pyams_content/component/paragraph/interfaces/pictogram.py:59
+#: src/pyams_content/component/paragraph/interfaces/keynumber.py:56
+msgid "Associated text"
+msgstr "Texte associé"
+
+#: src/pyams_content/component/paragraph/zmi/pictogram.py:349
 #: src/pyams_content/reference/pictograms/zmi/__init__.py:56
 msgid "Add pictogram"
 msgstr "Ajouter un pictogramme"
 
-#: src/pyams_content/component/paragraph/zmi/pictogram.py:418
+#: src/pyams_content/component/paragraph/zmi/pictogram.py:360
 #: src/pyams_content/reference/pictograms/zmi/__init__.py:67
 msgid "Add new pictogram"
 msgstr "Ajout d'un pictogramme"
 
-#: src/pyams_content/component/paragraph/zmi/pictogram.py:463
-#: src/pyams_content/reference/pictograms/zmi/__init__.py:94
+#: src/pyams_content/component/paragraph/zmi/pictogram.py:405
+#: src/pyams_content/reference/pictograms/zmi/__init__.py:95
 msgid "Edit pictogram properties"
 msgstr "Propriétés du pictogramme"
 
-#: src/pyams_content/component/paragraph/zmi/pictogram.py:447
+#: src/pyams_content/component/paragraph/zmi/pictogram.py:389
 msgid "Pictogram was correctly added"
 msgstr "Le pictogramme a été ajouté."
 
-#: src/pyams_content/component/paragraph/zmi/pictogram.py:371
-msgid "Given pictogram name doesn't exist!"
-msgstr "Le pictogramme indiqué n'existe pas !"
+#: src/pyams_content/component/paragraph/zmi/keynumber.py:76
+msgid "Key numbers..."
+msgstr "Chiffres-clés"
+
+#: src/pyams_content/component/paragraph/zmi/keynumber.py:87
+msgid "Add new key number paragraph"
+msgstr "Ajout de chiffres-clés"
+
+#: src/pyams_content/component/paragraph/zmi/keynumber.py:117
+msgid "Edit key number paragraph properties"
+msgstr "Propriétés des chiffres-clés"
+
+#: src/pyams_content/component/paragraph/zmi/keynumber.py:268
+#: src/pyams_content/component/paragraph/interfaces/keynumber.py:47
+msgid "Number"
+msgstr "Chiffre"
+
+#. Default: Header
+#: src/pyams_content/component/paragraph/zmi/keynumber.py:277
+#: src/pyams_content/component/paragraph/interfaces/keynumber.py:51
+msgid "key-number-label"
+msgstr "En-tête"
+
+#: src/pyams_content/component/paragraph/zmi/keynumber.py:317
+msgid "Add keynumber"
+msgstr "Ajouter un chiffre-clé"
+
+#: src/pyams_content/component/paragraph/zmi/keynumber.py:328
+msgid "Add new keynumber"
+msgstr "Ajout d'un chiffre-clé"
+
+#: src/pyams_content/component/paragraph/zmi/keynumber.py:368
+msgid "Edit keynumber properties"
+msgstr "Propriétés du chiffre-clé"
+
+#: src/pyams_content/component/paragraph/zmi/keynumber.py:352
+msgid "Key number was correctly added"
+msgstr "Le chiffre-clé a été ajouté."
 
 #: src/pyams_content/component/paragraph/zmi/frame.py:84
 msgid "Framed text..."
@@ -922,6 +948,7 @@
 #: src/pyams_content/component/paragraph/interfaces/milestone.py:41
 #: src/pyams_content/component/paragraph/interfaces/__init__.py:43
 #: src/pyams_content/component/paragraph/interfaces/pictogram.py:42
+#: src/pyams_content/component/paragraph/interfaces/keynumber.py:42
 #: src/pyams_content/component/association/interfaces/__init__.py:43
 #: src/pyams_content/shared/form/interfaces/__init__.py:86
 #: src/pyams_content/shared/site/interfaces/__init__.py:107
@@ -999,6 +1026,12 @@
 msgstr ""
 "Liste des types de paragraphes ajoutés automatiquement aux nouveaux contenus"
 
+#: src/pyams_content/component/paragraph/interfaces/video.py:41
+#: src/pyams_content/component/paragraph/interfaces/html.py:53
+#: src/pyams_content/component/video/interfaces/__init__.py:71
+msgid "Body"
+msgstr "Contenu HTML"
+
 #: src/pyams_content/component/paragraph/interfaces/video.py:53
 msgid "Video file content"
 msgstr ""
@@ -1028,19 +1061,16 @@
 msgstr "Sélection du pictogramme à afficher"
 
 #: src/pyams_content/component/paragraph/interfaces/pictogram.py:54
-msgid "Alternate label"
-msgstr "Libellé de substitution"
+msgid "Alternate header"
+msgstr "En-tête de substitution"
 
 #: src/pyams_content/component/paragraph/interfaces/pictogram.py:55
 msgid ""
-"Alternate pictogram label; if not specified, the pictogram title will be used"
+"Alternate pictogram label; if not specified, the pictogram header will be "
+"used"
 msgstr ""
-"Libellé de substitution utilisé par le pictogramme; si rien n'est spécifié, "
-"le titre du pictogramme sélectionné sera utilisé."
-
-#: src/pyams_content/component/paragraph/interfaces/pictogram.py:59
-msgid "Associated text"
-msgstr "Texte associé"
+"EN-tête de substitution utilisé par le pictogramme; si rien n'est spécifié, "
+"l'en-tête du pictogramme sélectionné sera utilisé."
 
 #: src/pyams_content/component/paragraph/interfaces/pictogram.py:60
 msgid "Additional text associated to this pictogram"
@@ -1054,6 +1084,34 @@
 msgid "Presentation template used for pictograms"
 msgstr "Modèle de présentation utilisé par ce paragraphe"
 
+#: src/pyams_content/component/paragraph/interfaces/keynumber.py:43
+msgid "Is this key number visible in front-office?"
+msgstr "Si 'non', ce chiffre-clé ne sera pas présenté aux internautes"
+
+#: src/pyams_content/component/paragraph/interfaces/keynumber.py:48
+msgid "Key number value"
+msgstr "Chiffre"
+
+#: src/pyams_content/component/paragraph/interfaces/keynumber.py:52
+msgid ""
+"Small text to be displayed above number (according to selected renderer)"
+msgstr ""
+"Texte court affiché au-dessus du chiffre (selon le mode de rendu sélectionné)"
+
+#: src/pyams_content/component/paragraph/interfaces/keynumber.py:57
+msgid "The way this text will be rendered depends on presentation template"
+msgstr ""
+"La présentation de cette information peut varier en fonction du mode de "
+"rendu choisi"
+
+#: src/pyams_content/component/paragraph/interfaces/keynumber.py:84
+msgid "Key numbers template"
+msgstr "Mode de rendu"
+
+#: src/pyams_content/component/paragraph/interfaces/keynumber.py:85
+msgid "Presentation template used for key numbers"
+msgstr "Modèle de présentation utilisé par ce paragraphe"
+
 #: src/pyams_content/component/paragraph/interfaces/frame.py:40
 msgid "Frame body"
 msgstr "Contenu"
@@ -1196,7 +1254,7 @@
 msgstr "Associations"
 
 #: src/pyams_content/component/association/container.py:95
-#: src/pyams_content/component/association/zmi/templates/associations.pt:6
+#: src/pyams_content/component/association/zmi/__init__.py:327
 msgid "Associations"
 msgstr "Associations"
 
@@ -1208,24 +1266,20 @@
 msgid "Edit association paragraph properties"
 msgstr "Propriétés du paragraphe « liens utiles »"
 
-#: src/pyams_content/component/association/zmi/__init__.py:176
-msgid "Switch association visibility"
-msgstr "Cliquez pour rendre le lien visible ou non en pied de paragraphe"
-
-#: src/pyams_content/component/association/zmi/__init__.py:227
+#: src/pyams_content/component/association/zmi/__init__.py:205
 msgid "Public title"
 msgstr "Libellé public"
 
-#: src/pyams_content/component/association/zmi/__init__.py:244
+#: src/pyams_content/component/association/zmi/__init__.py:222
 msgid "Inner title"
 msgstr "Contenu interne"
 
-#: src/pyams_content/component/association/zmi/__init__.py:259
+#: src/pyams_content/component/association/zmi/__init__.py:237
 msgid "Size"
 msgstr "Taille"
 
+#: src/pyams_content/component/association/zmi/__init__.py:304
 #: src/pyams_content/component/association/zmi/__init__.py:314
-#: src/pyams_content/component/association/zmi/__init__.py:324
 msgid "Associations list"
 msgstr "Liste des associations"
 
@@ -1233,6 +1287,10 @@
 msgid "Association was correctly added."
 msgstr "L'association a été ajoutée."
 
+#: src/pyams_content/component/association/zmi/__init__.py:276
+msgid "Given association name doesn't exist!"
+msgstr "Le nom d'association indiqué n'existe pas !"
+
 #: src/pyams_content/component/association/interfaces/__init__.py:44
 msgid "Is this item visible in front-office?"
 msgstr "Si 'non', ce lien ne sera pas présenté aux internautes"
@@ -2668,6 +2726,12 @@
 msgid "Name of this data type; must be unique between all data types"
 msgstr "Nom de ce type de donnée ; doit être unique entre tous les types"
 
+#: src/pyams_content/shared/common/interfaces/types.py:39
+#: src/pyams_content/shared/form/zmi/field.py:171
+#: src/pyams_content/shared/form/interfaces/__init__.py:61
+msgid "Label"
+msgstr "Libellé"
+
 #: src/pyams_content/shared/common/interfaces/types.py:42
 msgid "Navigation label"
 msgstr "Libellé de navigation"
@@ -4220,8 +4284,8 @@
 msgstr "Sélection de pictogrammes"
 
 #: src/pyams_content/reference/pictograms/zmi/manager.py:62
-#: src/pyams_content/reference/pictograms/zmi/templates/manager-selection.pt:26
-#: src/pyams_content/reference/pictograms/interfaces/__init__.py:65
+#: src/pyams_content/reference/pictograms/zmi/templates/manager-selection.pt:34
+#: src/pyams_content/reference/pictograms/interfaces/__init__.py:73
 msgid "Selected pictograms"
 msgstr "Pictogrammes sélectionnés"
 
@@ -4229,11 +4293,25 @@
 msgid "Available pictograms"
 msgstr "Pictogrammes disponibles"
 
+#: src/pyams_content/reference/pictograms/zmi/templates/manager-selection.pt:21
+#: src/pyams_content/reference/pictograms/zmi/templates/manager-selection.pt:48
+msgid "Display pictogram properties"
+msgstr "Propriétés du pictogramme"
+
 #: src/pyams_content/reference/pictograms/interfaces/__init__.py:45
 msgid "Pictogram content"
 msgstr "Utilisez le bouton \"Parcourir\" pour modifier le contenu de l'image"
 
-#: src/pyams_content/reference/pictograms/interfaces/__init__.py:66
+#. Default: Header
+#: src/pyams_content/reference/pictograms/interfaces/__init__.py:52
+msgid "pictogram-header"
+msgstr "En-tête par défaut"
+
+#: src/pyams_content/reference/pictograms/interfaces/__init__.py:53
+msgid "Default header associated with this pictogram"
+msgstr "En-tête par défaut associé à ce pictogramme"
+
+#: src/pyams_content/reference/pictograms/interfaces/__init__.py:74
 msgid "List of selected pictograms which will be available to shared contents"
 msgstr "Liste des pictogrammes proposés dans les contenus partagés"
 
@@ -4606,6 +4684,21 @@
 msgid "Thank you."
 msgstr "Merci."
 
+#~ msgid "Switch milestone visibility"
+#~ msgstr "Cliquez pour rendre le jalon visible ou non"
+
+#~ msgid "Switch paragraph visibility"
+#~ msgstr "Cliquez pour rendre le paragraphe visible ou non"
+
+#~ msgid "Switch pictogram visibility"
+#~ msgstr "Cliquez pour rendre le paragraphe visible ou non"
+
+#~ msgid "Given pictogram name doesn't exist!"
+#~ msgstr "Le pictogramme indiqué n'existe pas !"
+
+#~ msgid "Switch association visibility"
+#~ msgstr "Cliquez pour rendre le lien visible ou non en pied de paragraphe"
+
 #~ msgid "Associated pictogram label"
 #~ msgstr "Libellé associé au pictogramme"
 
@@ -4691,9 +4784,6 @@
 #~ msgid "Images data"
 #~ msgstr "Image(s) à ajouter"
 
-#~ msgid "PIF number"
-#~ msgstr "Numéro PIF"
-
 #~ msgid "Content type"
 #~ msgstr "Type de contenu"
 
--- a/src/pyams_content/locales/pyams_content.pot	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/locales/pyams_content.pot	Wed Apr 11 16:46:31 2018 +0200
@@ -6,7 +6,7 @@
 msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE 1.0\n"
-"POT-Creation-Date: 2018-04-04 15:32+0200\n"
+"POT-Creation-Date: 2018-04-06 13:47+0200\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -109,9 +109,7 @@
 msgstr ""
 
 #: ./src/pyams_content/component/gallery/zmi/file.py:258
-#: ./src/pyams_content/component/paragraph/zmi/milestone.py:357
-#: ./src/pyams_content/component/paragraph/zmi/pictogram.py:363
-#: ./src/pyams_content/component/association/zmi/__init__.py:288
+#: ./src/pyams_content/component/association/zmi/__init__.py:268
 #: ./src/pyams_content/shared/common/zmi/types.py:208
 #: ./src/pyams_content/shared/common/zmi/types.py:457
 #: ./src/pyams_content/shared/imagemap/zmi/container.py:169
@@ -201,6 +199,7 @@
 #: ./src/pyams_content/component/gallery/interfaces/__init__.py:50
 #: ./src/pyams_content/component/extfile/interfaces/__init__.py:77
 #: ./src/pyams_content/component/illustration/interfaces/__init__.py:44
+#: ./src/pyams_content/reference/pictograms/interfaces/__init__.py:48
 msgid "Accessibility title"
 msgstr ""
 
@@ -279,8 +278,8 @@
 
 #: ./src/pyams_content/component/gallery/interfaces/__init__.py:94
 #: ./src/pyams_content/component/extfile/interfaces/__init__.py:36
-#: ./src/pyams_content/component/paragraph/zmi/milestone.py:301
-#: ./src/pyams_content/component/paragraph/zmi/container.py:225
+#: ./src/pyams_content/component/paragraph/zmi/milestone.py:269
+#: ./src/pyams_content/component/paragraph/zmi/container.py:223
 #: ./src/pyams_content/component/paragraph/interfaces/milestone.py:46
 #: ./src/pyams_content/component/links/zmi/reverse.py:71
 #: ./src/pyams_content/shared/common/zmi/dashboard.py:109
@@ -289,7 +288,7 @@
 #: ./src/pyams_content/shared/site/zmi/folder.py:64
 #: ./src/pyams_content/root/zmi/templates/advanced-search.pt:188
 #: ./src/pyams_content/interfaces/__init__.py:99
-#: ./src/pyams_content/reference/pictograms/zmi/__init__.py:171
+#: ./src/pyams_content/reference/pictograms/zmi/__init__.py:172
 msgid "Title"
 msgstr ""
 
@@ -451,6 +450,7 @@
 
 #: ./src/pyams_content/component/extfile/interfaces/__init__.py:78
 #: ./src/pyams_content/component/illustration/interfaces/__init__.py:45
+#: ./src/pyams_content/reference/pictograms/interfaces/__init__.py:49
 msgid "Alternate title used to describe image content"
 msgstr ""
 
@@ -516,7 +516,7 @@
 
 #: ./src/pyams_content/component/paragraph/milestone.py:206
 #: ./src/pyams_content/component/paragraph/milestone.py:228
-#: ./src/pyams_content/component/paragraph/zmi/templates/milestones.pt:6
+#: ./src/pyams_content/component/paragraph/zmi/milestone.py:320
 msgid "Milestones"
 msgstr ""
 
@@ -542,7 +542,7 @@
 msgstr ""
 
 #: ./src/pyams_content/component/paragraph/container.py:82
-#: ./src/pyams_content/component/paragraph/zmi/templates/paragraphs.pt:6
+#: ./src/pyams_content/component/paragraph/zmi/container.py:307
 msgid "Paragraphs"
 msgstr ""
 
@@ -552,7 +552,7 @@
 
 #: ./src/pyams_content/component/paragraph/pictogram.py:197
 #: ./src/pyams_content/component/paragraph/pictogram.py:219
-#: ./src/pyams_content/component/paragraph/zmi/templates/pictograms.pt:6
+#: ./src/pyams_content/component/paragraph/zmi/pictogram.py:334
 msgid "Pictograms"
 msgstr ""
 
@@ -564,6 +564,16 @@
 msgid "Selected pictogram is missing"
 msgstr ""
 
+#: ./src/pyams_content/component/paragraph/keynumber.py:190
+#: ./src/pyams_content/component/paragraph/keynumber.py:213
+#: ./src/pyams_content/component/paragraph/zmi/keynumber.py:302
+msgid "Key numbers"
+msgstr ""
+
+#: ./src/pyams_content/component/paragraph/keynumber.py:222
+msgid "Key numbers paragraph"
+msgstr ""
+
 #: ./src/pyams_content/component/paragraph/frame.py:52
 msgid "Framed text"
 msgstr ""
@@ -612,53 +622,44 @@
 msgid "Header paragraph"
 msgstr ""
 
-#: ./src/pyams_content/component/paragraph/zmi/milestone.py:80
+#: ./src/pyams_content/component/paragraph/zmi/milestone.py:77
 msgid "Milestones..."
 msgstr ""
 
-#: ./src/pyams_content/component/paragraph/zmi/milestone.py:91
+#: ./src/pyams_content/component/paragraph/zmi/milestone.py:88
 msgid "Add new milestone paragraph"
 msgstr ""
 
-#: ./src/pyams_content/component/paragraph/zmi/milestone.py:121
+#: ./src/pyams_content/component/paragraph/zmi/milestone.py:118
 msgid "Edit milestone paragraph properties"
 msgstr ""
 
-#: ./src/pyams_content/component/paragraph/zmi/milestone.py:265
-msgid "Switch milestone visibility"
-msgstr ""
-
-#: ./src/pyams_content/component/paragraph/zmi/milestone.py:310
+#: ./src/pyams_content/component/paragraph/zmi/milestone.py:278
 #: ./src/pyams_content/component/paragraph/interfaces/milestone.py:50
 msgid "Associated label"
 msgstr ""
 
-#: ./src/pyams_content/component/paragraph/zmi/milestone.py:319
+#: ./src/pyams_content/component/paragraph/zmi/milestone.py:287
 #: ./src/pyams_content/component/paragraph/interfaces/milestone.py:54
 msgid "Anchor"
 msgstr ""
 
-#: ./src/pyams_content/component/paragraph/zmi/milestone.py:401
+#: ./src/pyams_content/component/paragraph/zmi/milestone.py:335
 msgid "Add milestone"
 msgstr ""
 
-#: ./src/pyams_content/component/paragraph/zmi/milestone.py:412
+#: ./src/pyams_content/component/paragraph/zmi/milestone.py:346
 msgid "Add new milestone"
 msgstr ""
 
-#: ./src/pyams_content/component/paragraph/zmi/milestone.py:452
+#: ./src/pyams_content/component/paragraph/zmi/milestone.py:386
 msgid "Edit milestone properties"
 msgstr ""
 
-#: ./src/pyams_content/component/paragraph/zmi/milestone.py:436
+#: ./src/pyams_content/component/paragraph/zmi/milestone.py:370
 msgid "Milestone was correctly added"
 msgstr ""
 
-#: ./src/pyams_content/component/paragraph/zmi/milestone.py:365
-#: ./src/pyams_content/component/association/zmi/__init__.py:292
-msgid "Given association name doesn't exist!"
-msgstr ""
-
 #: ./src/pyams_content/component/paragraph/zmi/keypoint.py:52
 msgid "Key points..."
 msgstr ""
@@ -712,100 +713,125 @@
 msgid "HTML content"
 msgstr ""
 
-#: ./src/pyams_content/component/paragraph/zmi/container.py:71
+#: ./src/pyams_content/component/paragraph/zmi/container.py:74
 msgid "Paragraphs..."
 msgstr ""
 
-#: ./src/pyams_content/component/paragraph/zmi/container.py:163
-msgid "Switch paragraph visibility"
-msgstr ""
-
-#: ./src/pyams_content/component/paragraph/zmi/container.py:241
+#: ./src/pyams_content/component/paragraph/zmi/container.py:239
 msgid "Show/hide all paragraphs"
 msgstr ""
 
-#: ./src/pyams_content/component/paragraph/zmi/container.py:287
-#: ./src/pyams_content/component/paragraph/zmi/container.py:296
+#: ./src/pyams_content/component/paragraph/zmi/container.py:285
+#: ./src/pyams_content/component/paragraph/zmi/container.py:294
 msgid "Paragraphs list"
 msgstr ""
 
-#: ./src/pyams_content/component/paragraph/zmi/container.py:395
+#: ./src/pyams_content/component/paragraph/zmi/container.py:364
 #: ./src/pyams_content/component/association/zmi/paragraph.py:55
-#: ./src/pyams_content/component/association/zmi/__init__.py:104
+#: ./src/pyams_content/component/association/zmi/__init__.py:107
 msgid "Associations..."
 msgstr ""
 
-#: ./src/pyams_content/component/paragraph/zmi/container.py:407
+#: ./src/pyams_content/component/paragraph/zmi/container.py:376
 msgid "Paragraphs associations"
 msgstr ""
 
-#: ./src/pyams_content/component/paragraph/zmi/container.py:122
+#: ./src/pyams_content/component/paragraph/zmi/container.py:126
 msgid "No currently defined paragraph."
 msgstr ""
 
-#: ./src/pyams_content/component/paragraph/zmi/container.py:250
+#: ./src/pyams_content/component/paragraph/zmi/container.py:248
 msgid "Click to open/close all paragraphs editors"
 msgstr ""
 
-#: ./src/pyams_content/component/paragraph/zmi/container.py:263
+#: ./src/pyams_content/component/paragraph/zmi/container.py:261
 msgid "Click to open/close paragraph editor"
 msgstr ""
 
-#: ./src/pyams_content/component/paragraph/zmi/container.py:128
+#: ./src/pyams_content/component/paragraph/zmi/container.py:132
 msgid "Check allowed paragraph types to be able to create new paragraphs."
 msgstr ""
 
-#: ./src/pyams_content/component/paragraph/zmi/pictogram.py:81
+#: ./src/pyams_content/component/paragraph/zmi/pictogram.py:84
 msgid "Pictograms..."
 msgstr ""
 
-#: ./src/pyams_content/component/paragraph/zmi/pictogram.py:92
+#: ./src/pyams_content/component/paragraph/zmi/pictogram.py:95
 msgid "Add new pictogram paragraph"
 msgstr ""
 
-#: ./src/pyams_content/component/paragraph/zmi/pictogram.py:122
+#: ./src/pyams_content/component/paragraph/zmi/pictogram.py:125
 msgid "Edit pictogram paragraph properties"
 msgstr ""
 
-#: ./src/pyams_content/component/paragraph/zmi/pictogram.py:285
-msgid "Switch pictogram visibility"
-msgstr ""
-
-#: ./src/pyams_content/component/paragraph/zmi/pictogram.py:321
-#: ./src/pyams_content/shared/common/interfaces/types.py:39
-#: ./src/pyams_content/shared/form/zmi/field.py:171
-#: ./src/pyams_content/shared/form/interfaces/__init__.py:61
-msgid "Label"
-msgstr ""
-
-#: ./src/pyams_content/component/paragraph/zmi/pictogram.py:336
-#: ./src/pyams_content/component/paragraph/interfaces/video.py:41
-#: ./src/pyams_content/component/paragraph/interfaces/html.py:53
-#: ./src/pyams_content/component/video/interfaces/__init__.py:71
-msgid "Body"
-msgstr ""
-
-#: ./src/pyams_content/component/paragraph/zmi/pictogram.py:407
+#. Default: Header
+#: ./src/pyams_content/component/paragraph/zmi/pictogram.py:297
+msgid "pictogram-item-header"
+msgstr ""
+
+#: ./src/pyams_content/component/paragraph/zmi/pictogram.py:312
+#: ./src/pyams_content/component/paragraph/zmi/keynumber.py:286
+#: ./src/pyams_content/component/paragraph/interfaces/pictogram.py:59
+#: ./src/pyams_content/component/paragraph/interfaces/keynumber.py:56
+msgid "Associated text"
+msgstr ""
+
+#: ./src/pyams_content/component/paragraph/zmi/pictogram.py:349
 #: ./src/pyams_content/reference/pictograms/zmi/__init__.py:56
 msgid "Add pictogram"
 msgstr ""
 
-#: ./src/pyams_content/component/paragraph/zmi/pictogram.py:418
+#: ./src/pyams_content/component/paragraph/zmi/pictogram.py:360
 #: ./src/pyams_content/reference/pictograms/zmi/__init__.py:67
 msgid "Add new pictogram"
 msgstr ""
 
-#: ./src/pyams_content/component/paragraph/zmi/pictogram.py:463
-#: ./src/pyams_content/reference/pictograms/zmi/__init__.py:94
+#: ./src/pyams_content/component/paragraph/zmi/pictogram.py:405
+#: ./src/pyams_content/reference/pictograms/zmi/__init__.py:95
 msgid "Edit pictogram properties"
 msgstr ""
 
-#: ./src/pyams_content/component/paragraph/zmi/pictogram.py:447
+#: ./src/pyams_content/component/paragraph/zmi/pictogram.py:389
 msgid "Pictogram was correctly added"
 msgstr ""
 
-#: ./src/pyams_content/component/paragraph/zmi/pictogram.py:371
-msgid "Given pictogram name doesn't exist!"
+#: ./src/pyams_content/component/paragraph/zmi/keynumber.py:76
+msgid "Key numbers..."
+msgstr ""
+
+#: ./src/pyams_content/component/paragraph/zmi/keynumber.py:87
+msgid "Add new key number paragraph"
+msgstr ""
+
+#: ./src/pyams_content/component/paragraph/zmi/keynumber.py:117
+msgid "Edit key number paragraph properties"
+msgstr ""
+
+#: ./src/pyams_content/component/paragraph/zmi/keynumber.py:268
+#: ./src/pyams_content/component/paragraph/interfaces/keynumber.py:47
+msgid "Number"
+msgstr ""
+
+#. Default: Header
+#: ./src/pyams_content/component/paragraph/zmi/keynumber.py:277
+#: ./src/pyams_content/component/paragraph/interfaces/keynumber.py:51
+msgid "key-number-label"
+msgstr ""
+
+#: ./src/pyams_content/component/paragraph/zmi/keynumber.py:317
+msgid "Add keynumber"
+msgstr ""
+
+#: ./src/pyams_content/component/paragraph/zmi/keynumber.py:328
+msgid "Add new keynumber"
+msgstr ""
+
+#: ./src/pyams_content/component/paragraph/zmi/keynumber.py:368
+msgid "Edit keynumber properties"
+msgstr ""
+
+#: ./src/pyams_content/component/paragraph/zmi/keynumber.py:352
+msgid "Key number was correctly added"
 msgstr ""
 
 #: ./src/pyams_content/component/paragraph/zmi/frame.py:84
@@ -883,6 +909,7 @@
 #: ./src/pyams_content/component/paragraph/interfaces/milestone.py:41
 #: ./src/pyams_content/component/paragraph/interfaces/__init__.py:43
 #: ./src/pyams_content/component/paragraph/interfaces/pictogram.py:42
+#: ./src/pyams_content/component/paragraph/interfaces/keynumber.py:42
 #: ./src/pyams_content/component/association/interfaces/__init__.py:43
 #: ./src/pyams_content/shared/form/interfaces/__init__.py:86
 #: ./src/pyams_content/shared/site/interfaces/__init__.py:107
@@ -955,6 +982,12 @@
 msgid "List of paragraphs automatically added to a new content"
 msgstr ""
 
+#: ./src/pyams_content/component/paragraph/interfaces/video.py:41
+#: ./src/pyams_content/component/paragraph/interfaces/html.py:53
+#: ./src/pyams_content/component/video/interfaces/__init__.py:71
+msgid "Body"
+msgstr ""
+
 #: ./src/pyams_content/component/paragraph/interfaces/video.py:53
 msgid "Video file content"
 msgstr ""
@@ -983,16 +1016,13 @@
 msgstr ""
 
 #: ./src/pyams_content/component/paragraph/interfaces/pictogram.py:54
-msgid "Alternate label"
+msgid "Alternate header"
 msgstr ""
 
 #: ./src/pyams_content/component/paragraph/interfaces/pictogram.py:55
 msgid ""
-"Alternate pictogram label; if not specified, the pictogram title will be used"
-msgstr ""
-
-#: ./src/pyams_content/component/paragraph/interfaces/pictogram.py:59
-msgid "Associated text"
+"Alternate pictogram label; if not specified, the pictogram header will be "
+"used"
 msgstr ""
 
 #: ./src/pyams_content/component/paragraph/interfaces/pictogram.py:60
@@ -1007,6 +1037,31 @@
 msgid "Presentation template used for pictograms"
 msgstr ""
 
+#: ./src/pyams_content/component/paragraph/interfaces/keynumber.py:43
+msgid "Is this key number visible in front-office?"
+msgstr ""
+
+#: ./src/pyams_content/component/paragraph/interfaces/keynumber.py:48
+msgid "Key number value"
+msgstr ""
+
+#: ./src/pyams_content/component/paragraph/interfaces/keynumber.py:52
+msgid ""
+"Small text to be displayed above number (according to selected renderer)"
+msgstr ""
+
+#: ./src/pyams_content/component/paragraph/interfaces/keynumber.py:57
+msgid "The way this text will be rendered depends on presentation template"
+msgstr ""
+
+#: ./src/pyams_content/component/paragraph/interfaces/keynumber.py:84
+msgid "Key numbers template"
+msgstr ""
+
+#: ./src/pyams_content/component/paragraph/interfaces/keynumber.py:85
+msgid "Presentation template used for key numbers"
+msgstr ""
+
 #: ./src/pyams_content/component/paragraph/interfaces/frame.py:40
 msgid "Frame body"
 msgstr ""
@@ -1147,7 +1202,7 @@
 msgstr ""
 
 #: ./src/pyams_content/component/association/container.py:95
-#: ./src/pyams_content/component/association/zmi/templates/associations.pt:6
+#: ./src/pyams_content/component/association/zmi/__init__.py:327
 msgid "Associations"
 msgstr ""
 
@@ -1159,24 +1214,20 @@
 msgid "Edit association paragraph properties"
 msgstr ""
 
-#: ./src/pyams_content/component/association/zmi/__init__.py:176
-msgid "Switch association visibility"
-msgstr ""
-
-#: ./src/pyams_content/component/association/zmi/__init__.py:227
+#: ./src/pyams_content/component/association/zmi/__init__.py:205
 msgid "Public title"
 msgstr ""
 
-#: ./src/pyams_content/component/association/zmi/__init__.py:244
+#: ./src/pyams_content/component/association/zmi/__init__.py:222
 msgid "Inner title"
 msgstr ""
 
-#: ./src/pyams_content/component/association/zmi/__init__.py:259
+#: ./src/pyams_content/component/association/zmi/__init__.py:237
 msgid "Size"
 msgstr ""
 
+#: ./src/pyams_content/component/association/zmi/__init__.py:304
 #: ./src/pyams_content/component/association/zmi/__init__.py:314
-#: ./src/pyams_content/component/association/zmi/__init__.py:324
 msgid "Associations list"
 msgstr ""
 
@@ -1184,6 +1235,10 @@
 msgid "Association was correctly added."
 msgstr ""
 
+#: ./src/pyams_content/component/association/zmi/__init__.py:276
+msgid "Given association name doesn't exist!"
+msgstr ""
+
 #: ./src/pyams_content/component/association/interfaces/__init__.py:44
 msgid "Is this item visible in front-office?"
 msgstr ""
@@ -2530,6 +2585,12 @@
 msgid "Name of this data type; must be unique between all data types"
 msgstr ""
 
+#: ./src/pyams_content/shared/common/interfaces/types.py:39
+#: ./src/pyams_content/shared/form/zmi/field.py:171
+#: ./src/pyams_content/shared/form/interfaces/__init__.py:61
+msgid "Label"
+msgstr ""
+
 #: ./src/pyams_content/shared/common/interfaces/types.py:42
 msgid "Navigation label"
 msgstr ""
@@ -4012,8 +4073,8 @@
 msgstr ""
 
 #: ./src/pyams_content/reference/pictograms/zmi/manager.py:62
-#: ./src/pyams_content/reference/pictograms/zmi/templates/manager-selection.pt:26
-#: ./src/pyams_content/reference/pictograms/interfaces/__init__.py:65
+#: ./src/pyams_content/reference/pictograms/zmi/templates/manager-selection.pt:34
+#: ./src/pyams_content/reference/pictograms/interfaces/__init__.py:73
 msgid "Selected pictograms"
 msgstr ""
 
@@ -4021,11 +4082,25 @@
 msgid "Available pictograms"
 msgstr ""
 
+#: ./src/pyams_content/reference/pictograms/zmi/templates/manager-selection.pt:21
+#: ./src/pyams_content/reference/pictograms/zmi/templates/manager-selection.pt:48
+msgid "Display pictogram properties"
+msgstr ""
+
 #: ./src/pyams_content/reference/pictograms/interfaces/__init__.py:45
 msgid "Pictogram content"
 msgstr ""
 
-#: ./src/pyams_content/reference/pictograms/interfaces/__init__.py:66
+#. Default: Header
+#: ./src/pyams_content/reference/pictograms/interfaces/__init__.py:52
+msgid "pictogram-header"
+msgstr ""
+
+#: ./src/pyams_content/reference/pictograms/interfaces/__init__.py:53
+msgid "Default header associated with this pictogram"
+msgstr ""
+
+#: ./src/pyams_content/reference/pictograms/interfaces/__init__.py:74
 msgid "List of selected pictograms which will be available to shared contents"
 msgstr ""
 
--- a/src/pyams_content/profile/zmi/__init__.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/profile/zmi/__init__.py	Wed Apr 11 16:46:31 2018 +0200
@@ -37,6 +37,8 @@
 class AdminProfileTabForm(InnerAdminEditForm):
     """Admin profile tab form"""
 
+    prefix = 'admin_profile.'
+
     tab_label = _("Admin. profile")
     legend = None
 
--- a/src/pyams_content/reference/pictograms/zmi/__init__.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/reference/pictograms/zmi/__init__.py	Wed Apr 11 16:46:31 2018 +0200
@@ -12,6 +12,7 @@
 
 __docformat__ = 'restructuredtext'
 
+
 # import standard library
 import sys
 
@@ -34,9 +35,11 @@
 from pyams_content.reference.pictograms import Pictogram
 from pyams_i18n.column import I18nAttrColumn
 from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.event import get_json_table_row_refresh_event
 from pyams_skin.table import I18nColumn
 from pyams_skin.viewlet.toolbar import ToolbarAction
 from pyams_utils.adapter import adapter_config
+from pyams_utils.traversing import get_parent
 from pyams_utils.url import absolute_url
 from pyams_viewlet.viewlet import viewlet_config
 from pyams_zmi.form import AdminDialogAddForm, AdminDialogEditForm
@@ -92,6 +95,8 @@
 class PictogramEditForm(AdminDialogEditForm):
     """Pictogram properties edit form"""
 
+    prefix = 'pictogram_properties.'
+
     legend = _("Edit pictogram properties")
     dialog_class = 'modal-large'
 
@@ -107,29 +112,11 @@
 
     def get_ajax_output(self, changes):
         output = super(PictogramAJAXEditForm, self).get_ajax_output(changes)
-        if 'image' in changes.get(IPictogram, ()):
-            image = II18n(self.context).query_attribute('image', request=self.request)
-            timestamp = randint(0, sys.maxsize)
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'MyAMS.skin.refreshRowCell',
-                    'object_id': '{0}::{1}'.format(ReferenceTableContentsTable.id, self.context.__name__),
-                    'col_name': 'image',
-                    'cell': '<img src="{0}?_={1}" />'.format(absolute_url(image, self.request, '++thumb++32x32'),
-                                                             timestamp)
-                }
-            })
-        if 'title' in changes.get(IBaseContent, ()):
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'MyAMS.skin.refreshRowCell',
-                    'object_id': '{0}::{1}'.format(ReferenceTableContentsTable.id, self.context.__name__),
-                    'col_name': 'name',
-                    'cell': II18n(self.context).query_attribute('title', request=self.request)
-                }
-            })
+        if ('image' in changes.get(IPictogram, ())) or \
+           ('title' in changes.get(IBaseContent, ())):
+            parent = get_parent(self.context, IPictogramTable)
+            output.setdefault('events', []).append(
+                get_json_table_row_refresh_event(parent, self.request, PictogramTableContentsTable, self.context))
         return output
 
 
@@ -140,6 +127,8 @@
 class PictogramTableContentsTable(ReferenceTableContentsTable):
     """Pictograms table contents table"""
 
+    prefix = 'pictograms_table'
+
     @property
     def data_attributes(self):
         attributes = super(PictogramTableContentsTable, self).data_attributes
@@ -162,7 +151,8 @@
     def getValue(self, obj):
         image = II18n(obj).query_attribute('image', request=self.request)
         if image:
-            return '<img src="{0}" />'.format(absolute_url(image, self.request, '++thumb++32x32'))
+            timestamp = randint(0, sys.maxsize)
+            return '<img src="{0}?_={1}" />'.format(absolute_url(image, self.request, '++thumb++32x32'), timestamp)
 
 
 @adapter_config(name='name', context=(IPictogramTable, IAdminLayer, PictogramTableContentsTable), provides=IColumn)
--- a/src/pyams_content/reference/zmi/__init__.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/reference/zmi/__init__.py	Wed Apr 11 16:46:31 2018 +0200
@@ -16,7 +16,7 @@
 # import standard library
 
 # import interfaces
-from pyams_content.interfaces import MANAGE_SITE_ROOT_PERMISSION
+from pyams_content.interfaces import MANAGE_SITE_ROOT_PERMISSION, IBaseContent
 from pyams_content.reference.interfaces import IReferenceManager, IReferenceInfo
 from pyams_i18n.interfaces import II18n
 from pyams_skin.interfaces.container import ITableElementName
@@ -25,10 +25,15 @@
 from pyams_zmi.layer import IAdminLayer
 
 # import packages
+from pyams_content.reference import IReferenceTable
+from pyams_content.reference.zmi.table import ReferenceTableContentsTable
+from pyams_form.form import AJAXEditForm
+from pyams_skin.event import get_json_table_row_refresh_event
 from pyams_skin.viewlet.breadcrumb import BreadcrumbItem
 from pyams_skin.viewlet.toplinks import TopLinksViewlet, TopLinksMenu
 from pyams_utils.adapter import adapter_config, ContextRequestAdapter
 from pyams_utils.registry import get_local_registry
+from pyams_utils.traversing import get_parent
 from pyams_utils.url import absolute_url
 from pyams_viewlet.viewlet import viewlet_config
 from zope.interface import Interface
@@ -72,3 +77,21 @@
     @property
     def name(self):
         return II18n(self.context).query_attribute('title', request=self.request)
+
+
+#
+# Reference table item views
+#
+
+class ReferenceInfoAJAXEditForm(AJAXEditForm):
+    """Base reference info edit form"""
+
+    table_factory = ReferenceTableContentsTable
+
+    def get_ajax_output(self, changes):
+        output = super(ReferenceInfoAJAXEditForm, self).get_ajax_output(changes)
+        if 'title' in changes.get(IBaseContent, ()):
+            parent = get_parent(self.context, IReferenceTable)
+            output.setdefault('events', []).append(
+                get_json_table_row_refresh_event(parent, self.request, self.table_factory, self.context))
+        return output
--- a/src/pyams_content/reference/zmi/table.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/reference/zmi/table.py	Wed Apr 11 16:46:31 2018 +0200
@@ -30,7 +30,7 @@
 # import packages
 from pyams_form.form import AJAXEditForm
 from pyams_pagelet.pagelet import pagelet_config
-from pyams_skin.container import ContainerView
+from pyams_skin.container import ContainerView, delete_container_element
 from pyams_skin.page import DefaultPageHeaderAdapter
 from pyams_skin.table import BaseTable, TrashColumn
 from pyams_skin.viewlet.breadcrumb import BreadcrumbItem
@@ -78,16 +78,16 @@
 class ReferenceTableContentsTable(BaseTable):
     """References table contents table"""
 
+    prefix = 'references'
+
     title = _("Table contents")
 
     @property
     def data_attributes(self):
         attributes = super(ReferenceTableContentsTable, self).data_attributes
-        table_attrs = {'data-ams-location': absolute_url(self.context, self.request)}
-        if 'table' in attributes:
-            attributes['table'].update(table_attrs)
-        else:
-            attributes['table'] = table_attrs
+        attributes.setdefault('table', {}).update({
+            'data-ams-location': absolute_url(self.context, self.request)
+        })
         return attributes
 
 
@@ -107,6 +107,13 @@
     permission = MANAGE_SITE_ROOT_PERMISSION
 
 
+@view_config(name='delete-element.json', context=IReferenceTable, request_type=IPyAMSLayer,
+             permission=MANAGE_SITE_ROOT_PERMISSION, renderer='json', xhr=True)
+def delete_reference(request):
+    """Delete selected reference item"""
+    return delete_container_element(request, ignore_permission=True)
+
+
 @pagelet_config(name='contents.html', context=IReferenceTable, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
 @implementer(IInnerPage)
 class ReferenceTableContentsView(AdminView, ContainerView):
@@ -144,6 +151,8 @@
 class ReferenceTablePropertiesEditForm(AdminDialogEditForm):
     """Reference table properties edit form"""
 
+    prefix = 'table_properties.'
+
     legend = _("Edit table properties")
 
     fields = field.Fields(IReferenceTable).omit('__parent__', '__name__')
--- a/src/pyams_content/root/__init__.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/root/__init__.py	Wed Apr 11 16:46:31 2018 +0200
@@ -18,7 +18,7 @@
 # import standard library
 
 # import interfaces
-from pyams_content.features.alert import IAlertTarget
+from pyams_content.features.alert.interfaces import IAlertTarget
 from pyams_content.features.preview.interfaces import IPreviewTarget
 from pyams_content.interfaces import WEBMASTER_ROLE, OPERATOR_ROLE
 from pyams_content.root.interfaces import ISiteRootRoles, ISiteRootConfiguration, ISiteRoot, \
--- a/src/pyams_content/root/zmi/search.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/root/zmi/search.py	Wed Apr 11 16:46:31 2018 +0200
@@ -70,12 +70,18 @@
     results = SiteRootQuickSearchResults(request.context, request)
     if len(results.values) == 1:
         result = results.values[0]
-        return {'status': 'redirect',
-                'location': absolute_url(result, request, 'admin')}
+        return {
+            'status': 'redirect',
+            'location': absolute_url(result, request, 'admin')
+        }
     else:
         results.update()
-        return {'status': 'info',
-                'content': {'html': results.render()}}
+        return {
+            'status': 'info',
+            'content': {
+                'html': results.render()
+            }
+        }
 
 
 @implementer(ISiteRootDashboardTable)
@@ -89,9 +95,10 @@
     @property
     def data_attributes(self):
         attributes = super(SiteRootQuickSearchResults, self).data_attributes
-        attributes['table'] = {'data-ams-datatable-sorting': '[]',
-                               'data-ams-datatable-display-length':
-                                   IAdminProfile(self.request.principal).table_page_length}
+        attributes['table'] = {
+            'data-ams-datatable-sorting': '[]',
+            'data-ams-datatable-display-length': IAdminProfile(self.request.principal).table_page_length
+        }
         return attributes
 
 
@@ -262,9 +269,10 @@
     @property
     def data_attributes(self):
         attributes = super(SiteRootAdvancedSearchResultsView, self).data_attributes
-        attributes['table'] = {'data-ams-datatable-sorting': '[]',
-                               'data-ams-datatable-display-length':
-                                   IAdminProfile(self.request.principal).table_page_length}
+        attributes['table'] = {
+            'data-ams-datatable-sorting': '[]',
+            'data-ams-datatable-display-length': IAdminProfile(self.request.principal).table_page_length
+        }
         return attributes
 
 
--- a/src/pyams_content/root/zmi/sites.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/root/zmi/sites.py	Wed Apr 11 16:46:31 2018 +0200
@@ -35,7 +35,7 @@
 # import packages
 from pyams_content.skin import pyams_content
 from pyams_pagelet.pagelet import pagelet_config
-from pyams_skin.container import ContainerView
+from pyams_skin.container import ContainerView, delete_container_element
 from pyams_skin.page import DefaultPageHeaderAdapter
 from pyams_skin.table import BaseTable, TrashColumn, ActionColumn, I18nColumn
 from pyams_skin.viewlet.menu import MenuItem
@@ -46,6 +46,7 @@
 from pyams_viewlet.manager import viewletmanager_config
 from pyams_viewlet.viewlet import viewlet_config
 from pyams_zmi.view import AdminView
+from pyramid.exceptions import NotFound
 from pyramid.view import view_config
 from z3c.table.column import GetAttrColumn
 from zope.interface import implementer
@@ -73,7 +74,7 @@
 class SiteTreeTable(BaseTable):
     """Site tree table"""
 
-    id = 'site_tree_table'
+    prefix = 'site_tree'
     title = _("Blogs and shared sites")
 
     sortOn = None
@@ -85,8 +86,7 @@
         attributes.setdefault('table', {}).update({
             'data-ams-plugins': 'pyams_content',
             'data-ams-plugin-pyams_content-src': get_resource_path(pyams_content),
-            'data-ams-location': absolute_url(self.context, self.request),
-            'data-ams-delete-target': 'delete-shared-site.json'
+            'data-ams-location': absolute_url(self.context, self.request)
         })
         attributes.setdefault('tr', {}).update({
             'id': lambda x, col: '{0}::{1}'.format(self.id, intids.queryId(x)),
@@ -155,6 +155,15 @@
         return super(SiteTreeTrashColumn, self).has_permission(item) and item.is_deletable()
 
 
+@view_config(name='delete-shared-site.json', context=ISiteRoot, request_type=IPyAMSLayer,
+             permission=MANAGE_SITE_ROOT_PERMISSION, renderer='json', xhr=True)
+def delete_shared_site(request):
+    """Delete shared site from site manager"""
+    if not request.context.is_deletable():
+        raise NotFound()
+    return delete_container_element(request, ignore_permission=True)
+
+
 @adapter_config(context=(ISiteRoot, IAdminLayer, SiteTreeTable), provides=IValues)
 class SiteTreValuesAdapter(ContextRequestViewAdapter):
     """Site tree values adapter"""
@@ -181,29 +190,3 @@
     """Site tree view header adapter"""
 
     icon_class = 'fa fa-fw fa-sitemap'
-
-
-@view_config(name='delete-shared-site.json', context=ISiteRoot, request_type=IPyAMSLayer,
-             permission=MANAGE_SITE_ROOT_PERMISSION, renderer='json', xhr=True)
-def delete_shared_site(request):
-    """Delete shared site from site manager"""
-    translate = request.localizer.translate
-    name = request.params.get('object_name')
-    if not name:
-        return {
-            'status': 'message',
-            'messagebox': {
-                'status': 'error',
-                'content': translate(_("No provided object_name argument!"))
-            }
-        }
-    if name not in request.context:
-        return {
-            'status': 'message',
-            'messagebox': {
-                'status': 'error',
-                'content': translate(_("Given site name doesn't exist!"))
-            }
-        }
-    del request.context[name]
-    return {'status': 'success'}
--- a/src/pyams_content/shared/blog/zmi/manager.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/shared/blog/zmi/manager.py	Wed Apr 11 16:46:31 2018 +0200
@@ -171,6 +171,8 @@
 class BlogManagerWorkflowPublicationEditForm(AdminDialogEditForm):
     """Blog manager workflow publication edit form"""
 
+    prefix = 'blog_publication.'
+
     legend = _("Update publication dates")
 
     fields = field.Fields(IWorkflowPublicationInfo).select('publication_effective_date', 'publication_expiration_date')
--- a/src/pyams_content/shared/common/zmi/__init__.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/shared/common/zmi/__init__.py	Wed Apr 11 16:46:31 2018 +0200
@@ -117,8 +117,10 @@
     """Shared event add form, JSON renderer"""
 
     def get_ajax_output(self, changes):
-        return {'status': 'redirect',
-                'location': self.nextURL()}
+        return {
+            'status': 'redirect',
+            'location': self.nextURL()
+        }
 
 
 @viewlet_config(name='wf-create-message', context=Interface, layer=IPyAMSLayer, view=SharedContentAddForm,
@@ -352,8 +354,10 @@
     """Shared content duplicate form, JSON renderer"""
 
     def get_ajax_output(self, changes):
-        return {'status': 'redirect',
-                'location': absolute_url(changes, self.request, 'admin')}
+        return {
+            'status': 'redirect',
+            'location': absolute_url(changes, self.request, 'admin')
+        }
 
 
 @viewlet_config(name='wf-duplicate-message', context=IWfSharedContent, layer=IPyAMSLayer,
--- a/src/pyams_content/shared/common/zmi/dashboard.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/shared/common/zmi/dashboard.py	Wed Apr 11 16:46:31 2018 +0200
@@ -91,10 +91,10 @@
     @property
     def data_attributes(self):
         attributes = super(BaseDashboardTable, self).data_attributes
-        attributes['table'] = {'data-ams-datatable-sorting': "{0},{1}".format(len(self.columns) - 1,
-                                                                              self.dt_sort_order),
-                               'data-ams-datatable-display-length':
-                                   IAdminProfile(self.request.principal).table_page_length}
+        attributes['table'] = {
+            'data-ams-datatable-sorting': "{0},{1}".format(len(self.columns) - 1, self.dt_sort_order),
+            'data-ams-datatable-display-length': IAdminProfile(self.request.principal).table_page_length
+        }
         return attributes
 
     @cached_property
--- a/src/pyams_content/shared/common/zmi/i18n.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/shared/common/zmi/i18n.py	Wed Apr 11 16:46:31 2018 +0200
@@ -16,8 +16,7 @@
 # import standard library
 
 # import interfaces
-from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
-from pyams_content.shared.common.interfaces import IWfSharedContent, IManagerRestrictions
+from pyams_content.shared.common.interfaces import IWfSharedContent
 from pyams_form.interfaces.form import IFormContextPermissionChecker
 from pyams_skin.layer import IPyAMSLayer
 
--- a/src/pyams_content/shared/common/zmi/properties.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/shared/common/zmi/properties.py	Wed Apr 11 16:46:31 2018 +0200
@@ -98,8 +98,10 @@
     def get_ajax_output(self, changes):
         wf_changes = changes.get(IBaseContent, ())
         if 'title' in wf_changes:
-            return {'status': 'reload',
-                    'message': self.request.localizer.translate(self.successMessage)}
+            return {
+                'status': 'reload',
+                'message': self.request.localizer.translate(self.successMessage)
+            }
         else:
             return super(SharedContentPropertiesAJAXEditForm, self).get_ajax_output(changes)
 
--- a/src/pyams_content/shared/common/zmi/search.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/shared/common/zmi/search.py	Wed Apr 11 16:46:31 2018 +0200
@@ -73,12 +73,16 @@
     results = SharedToolQuickSearchResults(request.context, request)
     if len(results.values) == 1:
         result = results.values[0]
-        return {'status': 'redirect',
-                'location': absolute_url(result, request, 'admin')}
+        return {
+            'status': 'redirect',
+            'location': absolute_url(result, request, 'admin')
+        }
     else:
         results.update()
-        return {'status': 'info',
-                'content': {'html': results.render()}}
+        return {
+            'status': 'info',
+            'content': {'html': results.render()}
+        }
 
 
 @implementer(ISharedToolDashboardTable)
@@ -92,9 +96,10 @@
     @property
     def data_attributes(self):
         attributes = super(SharedToolQuickSearchResults, self).data_attributes
-        attributes['table'] = {'data-ams-datatable-sorting': '[]',
-                               'data-ams-datatable-display-length':
-                                   IAdminProfile(self.request.principal).table_page_length}
+        attributes['table'] = {
+            'data-ams-datatable-sorting': '[]',
+            'data-ams-datatable-display-length': IAdminProfile(self.request.principal).table_page_length
+        }
         return attributes
 
 
@@ -286,9 +291,10 @@
     @property
     def data_attributes(self):
         attributes = super(SharedToolAdvancedSearchResultsView, self).data_attributes
-        attributes['table'] = {'data-ams-datatable-sorting': '[]',
-                               'data-ams-datatable-display-length':
-                                   IAdminProfile(self.request.principal).table_page_length}
+        attributes['table'] = {
+            'data-ams-datatable-sorting': '[]',
+            'data-ams-datatable-display-length': IAdminProfile(self.request.principal).table_page_length
+        }
         return attributes
 
 
--- a/src/pyams_content/shared/common/zmi/security.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/shared/common/zmi/security.py	Wed Apr 11 16:46:31 2018 +0200
@@ -166,6 +166,8 @@
 class SharedToolManagerRestrictionsEditForm(AdminDialogEditForm):
     """Shared tool manager restrictions edit form"""
 
+    prefix = 'tool_restrictions.'
+
     icon_css_class = 'fa fa-fw fa-lock'
 
     ajax_handler = 'manager-restrictions.json'
@@ -188,7 +190,8 @@
 
     @cached_property
     def principal_id(self):
-        principal_id = self.request.params.get('principal_id') or self.request.params.get('form.widgets.principal_id')
+        principal_id = self.request.params.get('principal_id') or \
+                       self.request.params.get('{0}{1}principal_id'.format(self.prefix, self.widgets.prefix))
         if not principal_id:
             raise NotFound
         return principal_id
--- a/src/pyams_content/shared/common/zmi/summary.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/shared/common/zmi/summary.py	Wed Apr 11 16:46:31 2018 +0200
@@ -14,7 +14,6 @@
 
 
 # import standard library
-from datetime import datetime
 
 # import interfaces
 from pyams_content.shared.common.interfaces import IWfSharedContent, IWfSharedContentRoles, IBaseSharedTool
--- a/src/pyams_content/shared/common/zmi/types.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/shared/common/zmi/types.py	Wed Apr 11 16:46:31 2018 +0200
@@ -36,6 +36,8 @@
 from pyams_form.form import AJAXAddForm, AJAXEditForm
 from pyams_form.security import ProtectedFormObjectMixin
 from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.container import delete_container_element
+from pyams_skin.event import get_json_table_refresh_event
 from pyams_skin.table import BaseTable, SorterColumn, TrashColumn, NameColumn, ActionColumn
 from pyams_skin.viewlet.menu import MenuItem
 from pyams_skin.viewlet.toolbar import ToolbarAction
@@ -74,7 +76,8 @@
 class TypedSharedToolTypesTable(ProtectedFormObjectMixin, BaseTable):
     """Typed shared tool types table"""
 
-    id = 'types_list'
+    prefix = 'types'
+
     hide_header = True
     sortOn = None
 
@@ -89,13 +92,14 @@
     @property
     def data_attributes(self):
         attributes = super(TypedSharedToolTypesTable, self).data_attributes
-        attributes['table'] = {'id': self.id,
-                               'data-ams-plugins': 'pyams_content',
-                               'data-ams-plugin-pyams_content-src':
-                                   get_resource_path(pyams_content),
-                               'data-ams-location': absolute_url(ITypedDataManager(self.context), self.request),
-                               'data-ams-tablednd-drag-handle': 'td.sorter',
-                               'data-ams-tablednd-drop-target': 'set-types-order.json'}
+        attributes['table'] = {
+            'id': self.id,
+            'data-ams-plugins': 'pyams_content',
+            'data-ams-plugin-pyams_content-src': get_resource_path(pyams_content),
+            'data-ams-location': absolute_url(ITypedDataManager(self.context), self.request),
+            'data-ams-tablednd-drag-handle': 'td.sorter',
+            'data-ams-tablednd-drop-target': 'set-types-order.json'
+        }
         return attributes
 
     @reify
@@ -124,6 +128,15 @@
     """Typed shared tool types sorter column"""
 
 
+@view_config(name='set-types-order.json', context=ITypedDataManager, request_type=IPyAMSLayer,
+             permission=MANAGE_TOOL_PERMISSION, renderer='json', xhr=True)
+def set_data_types_order(request):
+    """Update data types order"""
+    order = list(map(str, json.loads(request.params.get('names'))))
+    request.context.updateOrder(order)
+    return {'status': 'success'}
+
+
 @adapter_config(name='name', context=(ITypedSharedTool, IPyAMSLayer, TypedSharedToolTypesTable),
                 provides=IColumn)
 class TypedSharedToolTypesNameColumn(NameColumn):
@@ -183,6 +196,13 @@
     permission = MANAGE_TOOL_PERMISSION
 
 
+@view_config(name='delete-element.json', context=ITypedDataManager, request_type=IPyAMSLayer,
+             permission=MANAGE_TOOL_PERMISSION, renderer='json', xhr=True)
+def delete_data_type(request):
+    """Data type delete view"""
+    return delete_container_element(request, ignore_permission=True)
+
+
 @pagelet_config(name='data-types.html', context=ITypedSharedTool, layer=IPyAMSLayer,
                 permission=MANAGE_TOOL_PERMISSION)
 class TypedSharedToolTypesView(ContainerAdminView):
@@ -193,37 +213,6 @@
 
 
 #
-# Typed shared data types manager views
-#
-
-@view_config(name='delete-element.json', context=ITypedDataManager, request_type=IPyAMSLayer,
-             permission=MANAGE_TOOL_PERMISSION, renderer='json', xhr=True)
-def delete_data_type(request):
-    """Data type delete view"""
-    translate = request.localizer.translate
-    name = request.params.get('object_name')
-    if not name:
-        return {'status': 'message',
-                'messagebox': {'status': 'error',
-                               'content': translate(_("No provided object_name argument!"))}}
-    if name not in request.context:
-        return {'status': 'message',
-                'messagebox': {'status': 'error',
-                               'content': translate(_("Given data type doesn't exist!"))}}
-    del request.context[name]
-    return {'status': 'success'}
-
-
-@view_config(name='set-types-order.json', context=ITypedDataManager, request_type=IPyAMSLayer,
-             permission=MANAGE_TOOL_PERMISSION, renderer='json', xhr=True)
-def set_data_types_order(request):
-    """Update data types order"""
-    order = list(map(str, json.loads(request.params.get('names'))))
-    request.context.updateOrder(order)
-    return {'status': 'success'}
-
-
-#
 # Data type views
 #
 
@@ -297,6 +286,8 @@
 class DataTypeEditForm(AdminDialogEditForm):
     """Data type edit form"""
 
+    prefix = 'datatype_properties.'
+
     legend = _("Data type properties")
     icon_css_class = 'fa fa-fw fa-folder-o'
     label_css_class = 'control-label col-md-4'
@@ -326,9 +317,7 @@
 class DatatypeSubtypesTable(BaseTable):
     """Data type subtypes table"""
 
-    @property
-    def id(self):
-        return 'subtypes_{0}_list'.format(self.context.__name__)
+    prefix = 'subtypes'
 
     hide_header = True
     sortOn = None
@@ -339,14 +328,15 @@
     @property
     def data_attributes(self):
         attributes = super(DatatypeSubtypesTable, self).data_attributes
-        attributes['table'] = {'id': self.id,
-                               'data-ams-plugins': 'pyams_content',
-                               'data-ams-plugin-pyams_content-src':
-                                   get_resource_path(pyams_content),
-                               'data-ams-location': absolute_url(self.context, self.request),
-                               'data-ams-tablednd-drag-handle': 'td.sorter',
-                               'data-ams-tablednd-drop-target': 'set-subtypes-order.json'}
-        attributes.setdefault('tr', {}).setdefault('data-ams-stop-propagation', 'true')
+        attributes['table'] = {
+            'id': self.id,
+            'data-ams-plugins': 'pyams_content',
+            'data-ams-plugin-pyams_content-src': get_resource_path(pyams_content),
+            'data-ams-location': absolute_url(self.context, self.request),
+            'data-ams-tablednd-drag-handle': 'td.sorter',
+            'data-ams-tablednd-drop-target': 'set-subtypes-order.json'
+        }
+        attributes.setdefault('tr', {}).update({'data-ams-stop-propagation': 'true'})
         return attributes
 
     @reify
@@ -368,6 +358,15 @@
     """Data type subtypes table sorter column"""
 
 
+@view_config(name='set-subtypes-order.json', context=IDataType, request_type=IPyAMSLayer,
+             permission=MANAGE_TOOL_PERMISSION, renderer='json', xhr=True)
+def set_subtypes_order(request):
+    """Update subtypes order"""
+    order = list(map(str, json.loads(request.params.get('names'))))
+    request.context.updateOrder(order)
+    return {'status': 'success'}
+
+
 @adapter_config(name='name', context=(IDataType, IPyAMSLayer, DatatypeSubtypesTable), provides=IColumn)
 class DatatypeSubtypesTableNameColumn(NameColumn):
     """Data type subtypes table name column"""
@@ -424,6 +423,13 @@
     permission = MANAGE_TOOL_PERMISSION
 
 
+@view_config(name='delete-element.json', context=IDataType, request_type=IPyAMSLayer,
+             permission=MANAGE_TOOL_PERMISSION, renderer='json', xhr=True)
+def delete_subtype(request):
+    """Data subtype delete view"""
+    return delete_container_element(request, ignore_permission=True)
+
+
 @view_config(name='get-subtypes-table.json', context=ITypedDataManager, request_type=IPyAMSLayer,
              permission=MANAGE_TOOL_PERMISSION, renderer='json', xhr=True)
 def get_subtypes_table(request):
@@ -436,38 +442,10 @@
     return table.render()
 
 
-@view_config(name='set-subtypes-order.json', context=IDataType, request_type=IPyAMSLayer,
-             permission=MANAGE_TOOL_PERMISSION, renderer='json', xhr=True)
-def set_subtypes_order(request):
-    """Update subtypes order"""
-    order = list(map(str, json.loads(request.params.get('names'))))
-    request.context.updateOrder(order)
-    return {'status': 'success'}
-
-
-@view_config(name='delete-element.json', context=IDataType, request_type=IPyAMSLayer,
-             permission=MANAGE_TOOL_PERMISSION, renderer='json', xhr=True)
-def delete_subtype(request):
-    """Data subtype delete view"""
-    translate = request.localizer.translate
-    name = request.params.get('object_name')
-    if not name:
-        return {'status': 'message',
-                'messagebox': {'status': 'error',
-                               'content': translate(_("No provided object_name argument!"))}}
-    if name not in request.context:
-        return {'status': 'message',
-                'messagebox': {'status': 'error',
-                               'content': translate(_("Given data subtype doesn't exist!"))}}
-    del request.context[name]
-    return {'status': 'success'}
-
-
 #
 # Data sub-types views
 #
 
-
 @viewlet_config(name='add-data-subtype.action', context=IDataType, layer=IPyAMSLayer,
                 view=DatatypeSubtypesTable, manager=IWidgetTitleViewletManager,
                 permission=MANAGE_TOOL_PERMISSION, weight=1)
@@ -522,19 +500,12 @@
     """Data subtype add form, JSON renderer"""
 
     def get_ajax_output(self, changes):
-        subtypes_table = DatatypeSubtypesTable(self.context, self.request)
-        subtypes_table.update()
         return {
             'status': 'success',
             'message': self.request.localizer.translate(_("Subtype was correctly added.")),
-            'events': [{
-                'event': 'myams.refresh',
-                'options': {
-                    'handler': 'MyAMS.skin.refreshTable',
-                    'object_id': subtypes_table.id,
-                    'table': subtypes_table.render()
-                }
-            }]
+            'events': [
+                get_json_table_refresh_event(self.context, self.request, DatatypeSubtypesTable)
+            ]
         }
 
 
@@ -542,6 +513,8 @@
 class DataSubtypeEditForm(AdminDialogEditForm):
     """Data subtype edit form"""
 
+    prefix = 'subtype_properties.'
+
     legend = _("Data subtype properties")
     icon_css_class = 'fa fa-fw fa-folder-o'
     label_css_class = 'control-label col-md-4'
@@ -566,19 +539,12 @@
     def get_ajax_output(self, changes):
         if 'label' in changes.get(IBaseDataType, ()):
             target = get_parent(self.context, IDataType)
-            subtypes_table = DatatypeSubtypesTable(target, self.request)
-            subtypes_table.update()
             return {
                 'status': 'success',
                 'message': self.request.localizer.translate(self.successMessage),
-                'events': [{
-                    'event': 'myams.refresh',
-                    'options': {
-                        'handler': 'MyAMS.skin.refreshTable',
-                        'object_id': subtypes_table.id,
-                        'table': subtypes_table.render()
-                    }
-                }]
+                'events': [
+                    get_json_table_refresh_event(target, self.request, DatatypeSubtypesTable)
+                ]
             }
         else:
             return super(DataSubtypeAJAXEditForm, self).get_ajax_output(changes)
--- a/src/pyams_content/shared/common/zmi/workflow.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/shared/common/zmi/workflow.py	Wed Apr 11 16:46:31 2018 +0200
@@ -717,8 +717,10 @@
     """Shared content clone form, JSON rendener"""
 
     def get_ajax_output(self, changes):
-        return {'status': 'redirect',
-                'location': absolute_url(changes, self.request, 'admin#properties.html')}
+        return {
+            'status': 'redirect',
+            'location': absolute_url(changes, self.request, 'admin#properties.html')
+        }
 
 
 @viewlet_config(name='wf-clone-message', context=IWfSharedContent, layer=IPyAMSLayer,
@@ -801,8 +803,10 @@
     """Shared content delete form, JSON rendener"""
 
     def get_ajax_output(self, changes):
-        return {'status': 'redirect',
-                'location': absolute_url(self._SharedContentDeleteForm__target, self.request, 'admin')}
+        return {
+            'status': 'redirect',
+            'location': absolute_url(self._SharedContentDeleteForm__target, self.request, 'admin')
+        }
 
 
 @viewlet_config(name='wf-delete-message', context=IWfSharedContent, layer=IPyAMSLayer,
--- a/src/pyams_content/shared/form/zmi/field.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/shared/form/zmi/field.py	Wed Apr 11 16:46:31 2018 +0200
@@ -38,7 +38,9 @@
 from pyams_form.form import AJAXAddForm, AJAXEditForm
 from pyams_form.security import ProtectedFormObjectMixin
 from pyams_pagelet.pagelet import pagelet_config
-from pyams_skin.table import BaseTable, SorterColumn, JsActionColumn, I18nColumn, TrashColumn, I18nValueColumn
+from pyams_skin.container import switch_element_visibility
+from pyams_skin.table import BaseTable, SorterColumn, I18nColumn, TrashColumn, I18nValueColumn, \
+    VisibilitySwitcherColumn
 from pyams_skin.viewlet.menu import MenuItem
 from pyams_skin.viewlet.toolbar import ToolbarAction
 from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter
@@ -50,7 +52,6 @@
 from pyams_zmi.view import ContainerAdminView
 from pyramid.decorator import reify
 from pyramid.events import subscriber
-from pyramid.exceptions import NotFound
 from pyramid.view import view_config
 from z3c.form import field
 from z3c.table.column import GetAttrColumn
@@ -76,7 +77,8 @@
 class FormFieldsContainerTable(ProtectedFormObjectMixin, BaseTable):
     """Form fields table"""
 
-    id = 'form_fields_list'
+    prefix = 'form_fields'
+
     hide_header = True
     sortOn = None
 
@@ -91,13 +93,15 @@
     @property
     def data_attributes(self):
         attributes = super(FormFieldsContainerTable, self).data_attributes
-        attributes['table'] = {'id': self.id,
-                               'data-ams-plugins': 'pyams_content',
-                               'data-ams-plugin-pyams_content-src':
-                                   get_resource_path(pyams_content),
-                               'data-ams-location': absolute_url(IFormFieldContainer(self.context), self.request),
-                               'data-ams-tablednd-drag-handle': 'td.sorter',
-                               'data-ams-tablednd-drop-target': 'set-form-fields-order.json'}
+        attributes['table'] = {
+            'id': self.id,
+            'data-ams-plugins': 'pyams_content',
+            'data-ams-plugin-pyams_content-src': get_resource_path(pyams_content),
+            'data-ams-location': absolute_url(IFormFieldContainer(self.context), self.request),
+            'data-ams-tablednd-drag-handle': 'td.sorter',
+            'data-ams-tablednd-drop-target': 'set-form-fields-order.json',
+            'data-ams-visibility-switcher': 'switch-form-field-visibility.json'
+        }
         return attributes
 
     @reify
@@ -119,31 +123,15 @@
 
 @adapter_config(name='show-hide', context=(IFormFieldContainerTarget, IPyAMSLayer, FormFieldsContainerTable),
                 provides=IColumn)
-class FormFieldsContainerShowHideColumn(ProtectedFormObjectMixin, JsActionColumn):
+class FormFieldsContainerShowHideColumn(ProtectedFormObjectMixin, VisibilitySwitcherColumn):
     """Form fields container visibility switcher column"""
 
-    cssClasses = {'th': 'action',
-                  'td': 'action switcher'}
 
-    icon_class = 'fa fa-fw fa-eye'
-    icon_hint = _("Switch field visibility")
-
-    url = 'PyAMS_content.fields.switchVisibility'
-
-    weight = 5
-
-    def get_icon(self, item):
-        if item.visible:
-            icon_class = 'fa fa-fw fa-eye'
-        else:
-            icon_class = 'fa fa-fw fa-eye-slash text-danger'
-        return '<i class="{icon_class}"></i>'.format(icon_class=icon_class)
-
-    def renderCell(self, item):
-        if self.permission and not self.request.has_permission(self.permission, context=item):
-            return self.get_icon(item)
-        else:
-            return super(FormFieldsContainerShowHideColumn, self).renderCell(item)
+@view_config(name='switch-form-field-visibility.json', context=IFormFieldContainer, request_type=IPyAMSLayer,
+             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+def switch_form_field_visibility(request):
+    """Set form field visibility"""
+    return switch_element_visibility(request, IFormFieldContainer)
 
 
 @adapter_config(context=FormFieldsContainerShowHideColumn, provides=IFormSecurityContext)
@@ -225,19 +213,6 @@
     return {'status': 'success'}
 
 
-@view_config(name='set-form-field-visibility.json', context=IFormFieldContainer, request_type=IPyAMSLayer,
-             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
-def set_form_field_visibility(request):
-    """Set form field visibility"""
-    container = IFormFieldContainer(request.context)
-    paragraph = container.get(str(request.params.get('object_name')))
-    if paragraph is None:
-        raise NotFound()
-    field = IFormField(paragraph)
-    field.visible = not field.visible
-    return {'visible': field.visible}
-
-
 #
 # Form field views
 #
@@ -300,6 +275,8 @@
 class FormFieldPropertiesEditForm(AdminDialogEditForm):
     """Form field properties edit form"""
 
+    prefix = 'field_properties.'
+
     @property
     def title(self):
         content = get_parent(self.context, IWfSharedContent)
--- a/src/pyams_content/shared/form/zmi/properties.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/shared/form/zmi/properties.py	Wed Apr 11 16:46:31 2018 +0200
@@ -36,6 +36,8 @@
 class FormPropertiesEditForm(InnerAdminEditForm):
     """Form properties edit form extension"""
 
+    prefix = 'form_properties.'
+
     legend = _("Main form settings")
     fieldset_class = 'bordered no-x-margin margin-y-10'
 
@@ -49,8 +51,10 @@
 
     def get_ajax_output(self, changes):
         if 'handler' in changes.get(IWfForm, ()):
-            return {'status': 'reload',
-                    'message': self.request.localizer.translate(self.successMessage)}
+            return {
+                'status': 'reload',
+                'message': self.request.localizer.translate(self.successMessage)
+            }
         else:
             return super(FormPropertiesEditForm, self).get_ajax_output(changes)
 
@@ -61,6 +65,8 @@
 class FormHandlerPropertiesEditForm(InnerAdminEditForm):
     """Form handler properties edit form extension"""
 
+    prefix = 'form_handler.'
+
     legend = _("Form handler settings")
     fieldset_class = 'bordered no-x-margin margin-y-10'
 
--- a/src/pyams_content/shared/imagemap/zmi/area.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/shared/imagemap/zmi/area.py	Wed Apr 11 16:46:31 2018 +0200
@@ -103,6 +103,8 @@
 class ImagemapAreaPropertiesEditForm(AdminDialogEditForm):
     """Image map area properties edit form"""
 
+    prefix = 'area_properties.'
+
     legend = _("Edit image map properties")
 
     @property
--- a/src/pyams_content/shared/imagemap/zmi/container.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/shared/imagemap/zmi/container.py	Wed Apr 11 16:46:31 2018 +0200
@@ -76,6 +76,8 @@
 class ImagemapAreasTable(BaseTable):
     """Image map areas table"""
 
+    prefix = 'imagemaps'
+
     hide_header = True
     cssClasses = {'table': 'table table-bordered table-striped table-hover table-tight'}
 
@@ -87,9 +89,11 @@
     @property
     def data_attributes(self):
         attributes = super(ImagemapAreasTable, self).data_attributes
-        attributes['table'] = {'data-ams-location': absolute_url(self.context, self.request),
-                               'data-ams-datatable-sort': 'false',
-                               'data-ams-datatable-pagination': 'false'}
+        attributes['table'] = {
+            'data-ams-location': absolute_url(self.context, self.request),
+            'data-ams-datatable-sort': 'false',
+            'data-ams-datatable-pagination': 'false'
+        }
         return attributes
 
     @reify
@@ -164,19 +168,31 @@
     translate = request.localizer.translate
     name = request.params.get('object_name')
     if not name:
-        return {'status': 'message',
-                'messagebox': {'status': 'error',
-                               'content': translate(_("No provided object_name argument!"))}}
+        return {
+            'status': 'message',
+            'messagebox': {
+                'status': 'error',
+                'content': translate(_("No provided object_name argument!"))
+            }
+        }
     if '++' in name:
         try:
             name = name.split('++', 2)[2]
         except IndexError:
-            return {'status': 'message',
-                    'messagebox': {'status': 'error',
-                                   'content': translate(_("Bad query object_name parameter value!"))}}
+            return {
+                'status': 'message',
+                'messagebox': {
+                    'status': 'error',
+                    'content': translate(_("Bad query object_name parameter value!"))
+                }
+            }
     if name not in request.context.areas:
-        return {'status': 'message',
-                'messagebox': {'status': 'error',
-                               'content': translate(_("Given area name doesn't exist!"))}}
+        return {
+            'status': 'message',
+            'messagebox': {
+                'status': 'error',
+                'content': translate(_("Given area name doesn't exist!"))
+            }
+        }
     request.context.remove_area(name)
     return {'status': 'success'}
--- a/src/pyams_content/shared/imagemap/zmi/paragraph.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/shared/imagemap/zmi/paragraph.py	Wed Apr 11 16:46:31 2018 +0200
@@ -34,6 +34,7 @@
     BaseParagraphAddMenu, BaseParagraphPropertiesEditForm
 from pyams_content.shared.imagemap.paragraph import ImageMapParagraph
 from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.event import get_json_form_refresh_event
 from pyams_template.template import template_config
 from pyams_utils.adapter import adapter_config
 from pyams_utils.traversing import get_parent
@@ -87,6 +88,8 @@
 class ImagemapParagraphPropertiesEditForm(BaseParagraphPropertiesEditForm):
     """Image map paragraph edit form"""
 
+    prefix = 'imagemap_properties.'
+
     @property
     def title(self):
         content = get_parent(self.context, IWfSharedContent)
@@ -130,19 +133,8 @@
     def get_ajax_output(self, changes):
         output = super(ImagemapParagraphInnerAJAXEditForm, self).get_ajax_output(changes)
         if 'reference' in changes.get(IImageMapParagraph, ()):
-            form = ImagemapParagraphInnerEditForm(self.context, self.request)
-            form.update()
-            content = form.getContent()
-            output.setdefault('events', []).append({
-                'event': 'myams.refresh',
-                'options': {
-                    'object_id': '{0}_{1}_{2}'.format(
-                        content.__class__.__name__,
-                        getattr(content, '__name__', 'noname').replace('++', ''),
-                        form.id),
-                    'content': form.render()
-                }
-            })
+            output.setdefault('events', []).append(
+                get_json_form_refresh_event(self.context, self.request, ImagemapParagraphInnerEditForm))
         return output
 
 
--- a/src/pyams_content/shared/imagemap/zmi/properties.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/shared/imagemap/zmi/properties.py	Wed Apr 11 16:46:31 2018 +0200
@@ -35,6 +35,8 @@
 class ImageMapPropertiesEditForm(InnerAdminEditForm):
     """Image map properties edit form extension"""
 
+    prefix = 'imagemap.'
+
     legend = _("Background image")
     fieldset_class = 'bordered no-x-margin margin-y-10'
 
@@ -45,6 +47,7 @@
     def get_ajax_output(self, changes):
         if 'image' in changes.get(IWfImageMap, ()):
             translate = self.request.localizer.translate
-            return {'status': 'reload',
-                    'smallbox': translate(self.successMessage),
-                    'smallbox_status': 'success'}
+            return {
+                'status': 'reload',
+                'message': translate(self.successMessage)
+            }
--- a/src/pyams_content/shared/logo/zmi/paragraph.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/shared/logo/zmi/paragraph.py	Wed Apr 11 16:46:31 2018 +0200
@@ -35,9 +35,10 @@
 from pyams_content.features.renderer.zmi.widget import RendererFieldWidget
 from pyams_content.shared.logo.paragraph import LogosParagraph
 from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.event import get_json_widget_refresh_event
 from pyams_utils.adapter import adapter_config
 from pyams_utils.traversing import get_parent
-from pyams_viewlet.viewlet import viewlet_config, BaseContentProvider
+from pyams_viewlet.viewlet import viewlet_config
 from pyams_zmi.form import AdminDialogAddForm
 from pyramid.view import view_config
 from z3c.form import field, button
@@ -87,6 +88,8 @@
 class LogosParagraphPropertiesEditForm(BaseParagraphPropertiesEditForm):
     """Logos paragraph properties edit form"""
 
+    prefix = 'logos_properties.'
+
     @property
     def title(self):
         content = get_parent(self.context, IWfSharedContent)
@@ -133,21 +136,9 @@
         output = super(LogosParagraphInnerAJAXEditForm, self).get_ajax_output(changes)
         updated = changes.get(ILogosParagraph, ())
         if 'renderer' in updated:
-            form = LogosParagraphInnerEditForm(self.context, self.request)
-            form.update()
-            content = form.getContent()
-            output.setdefault('events', []).append({
-                'evennt': 'myams.refresh',
-                'options': {
-                    'handler': 'MyAMS.skin.refreshWidget',
-                    'parent_id': '{0}_{1}_{2}'.format(
-                        content.__class__.__name__,
-                        getattr(content, '__name__', 'noname').replace('++', ''),
-                        form.id),
-                    'widget_name': form.widgets['renderer'].name,
-                    'content': form.widgets['renderer'].render()
-                }
-            })
+            output.setdefault('events', []).append(
+                get_json_widget_refresh_event(self.context, self.request,
+                                              LogosParagraphInnerEditForm, 'renderer'))
         return output
 
 
--- a/src/pyams_content/shared/logo/zmi/properties.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/shared/logo/zmi/properties.py	Wed Apr 11 16:46:31 2018 +0200
@@ -35,6 +35,8 @@
 class LogoPropertiesEditForm(InnerAdminEditForm):
     """Logo properties edit form extension"""
 
+    prefix = 'logo_settings.'
+
     legend = _("Main logo settings")
     fieldset_class = 'bordered no-x-margin margin-y-10'
 
@@ -43,7 +45,9 @@
 
     def get_ajax_output(self, changes):
         if 'image' in changes.get(IWfLogo, ()):
-            return {'status': 'reload',
-                    'message': self.request.localizer.translate(self.successMessage)}
+            return {
+                'status': 'reload',
+                'message': self.request.localizer.translate(self.successMessage)
+            }
         else:
             return super(LogoPropertiesEditForm, self).get_ajax_output(changes)
--- a/src/pyams_content/shared/site/__init__.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/shared/site/__init__.py	Wed Apr 11 16:46:31 2018 +0200
@@ -21,12 +21,12 @@
 from pyams_content.component.theme.interfaces import IThemesTarget
 from pyams_content.features.preview.interfaces import IPreviewTarget
 from pyams_content.features.review.interfaces import IReviewTarget
+from pyams_content.shared.site.interfaces import ITopic, IWfTopic, TOPIC_CONTENT_NAME, \
+    TOPIC_CONTENT_TYPE, IWfTopicFactory
 from pyams_workflow.interfaces import IWorkflow, IWorkflowVersions, IWorkflowState
 
 # import packages
 from pyams_content.shared.common import SharedContent, WfSharedContent, register_content_type, IWfSharedContentFactory
-from pyams_content.shared.site.interfaces import ISiteContainer, ISiteFolder, ITopic, IWfTopic, TOPIC_CONTENT_NAME, \
-    TOPIC_CONTENT_TYPE, IWfTopicFactory
 from pyams_utils.adapter import adapter_config
 from zope.interface import implementer, provider
 
--- a/src/pyams_content/shared/site/link.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/shared/site/link.py	Wed Apr 11 16:46:31 2018 +0200
@@ -17,7 +17,6 @@
 
 # import interfaces
 from pyams_content.shared.site.interfaces import IContentLink
-from pyams_sequence.interfaces import ISequentialIdInfo
 from pyams_workflow.interfaces import IWorkflow, IWorkflowVersion, IWorkflowVersions, IWorkflowPublicationInfo, \
     IWorkflowState
 
--- a/src/pyams_content/shared/site/manager.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/shared/site/manager.py	Wed Apr 11 16:46:31 2018 +0200
@@ -24,7 +24,7 @@
 from pyams_content.reference.pictograms.interfaces import IPictogramManagerTarget
 from pyams_content.root.interfaces import ISiteRoot
 from pyams_content.shared.common.interfaces import ISharedContentFactory
-from pyams_content.shared.site.interfaces import ISiteManager, ISiteManagerFactory, ISiteFolderFactory
+from pyams_content.shared.site.interfaces import ISiteManager, ISiteManagerFactory, ISiteFolderFactory, ISiteContainer
 from pyams_form.interfaces.form import IFormContextPermissionChecker
 from pyams_i18n.interfaces import II18n
 from pyams_portal.interfaces import IPortalContext
@@ -34,7 +34,7 @@
 
 # import packages
 from pyams_content.shared.common.manager import BaseSharedTool
-from pyams_content.shared.site import Topic, ISiteContainer
+from pyams_content.shared.site import Topic
 from pyams_content.shared.site.container import SiteContainerMixin
 from pyams_content.shared.site.folder import SiteFolder
 from pyams_skin.skin import UserSkinnableContent
--- a/src/pyams_content/shared/site/zmi/container.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/shared/site/zmi/container.py	Wed Apr 11 16:46:31 2018 +0200
@@ -43,7 +43,8 @@
 from pyams_content.skin import pyams_content
 from pyams_form.form import AJAXEditForm
 from pyams_pagelet.pagelet import pagelet_config
-from pyams_skin.container import ContainerView
+from pyams_skin.container import ContainerView, delete_container_element
+from pyams_skin.event import get_json_table_cell_refresh_event
 from pyams_skin.page import DefaultPageHeaderAdapter
 from pyams_skin.table import BaseTable, TrashColumn, DefaultElementEditorAdapter, NameColumn, SorterColumn, \
     JsActionColumn
@@ -121,6 +122,8 @@
 class SiteContainerWorkflowPublicationEditForm(AdminDialogEditForm):
     """Site container workflow publication edit form"""
 
+    prefix = 'site_publication.'
+
     legend = _("Update publication dates")
 
     fields = field.Fields(IWorkflowPublicationInfo).select('publication_effective_date', 'publication_expiration_date')
@@ -147,15 +150,11 @@
                             icon_class=icon_class,
                             title=self.request.localizer.translate(_("Visible element?")))
                 intids = get_utility(IIntIds)
-                output.setdefault('events', []).append({
-                    'event': 'myams.refresh',
-                    'options': {
-                        'handler': 'MyAMS.skin.refreshRowCell',
-                        'object_id': '{0}::{1}'.format(SiteContainerTreeTable.id, intids.queryId(self.context)),
-                        'col_name': 'visible',
-                        'cell': value
-                    }
-                })
+                output.setdefault('events', []).append(
+                    get_json_table_cell_refresh_event(self.context, self.request,
+                                                      '{0}::{1}'.format(SiteContainerTreeTable.id, intids.queryId(self.context)),
+                                                      'visible',
+                                                      value))
         return output
 
 
@@ -211,7 +210,6 @@
             'data-ams-location': absolute_url(self.context, self.request),
             'data-ams-datatable-sort': 'false',
             'data-ams-datatable-pagination': 'false',
-            'data-ams-delete-target': 'delete-site-item.json',
             'data-ams-tree-node-id': intids.queryId(manager),
             'data-ams-tablednd-drag-handle': 'td.sorter',
             'data-ams-tablednd-drop': 'MyAMS.tree.sortTree',
@@ -239,6 +237,59 @@
             return ''
 
 
+@view_config(name='set-site-order.json', context=ISiteContainer, request_type=IPyAMSLayer,
+             permission=MANAGE_SITE_PERMISSION, renderer='json', xhr=True)
+def set_site_order(request):
+    """Set site elements order"""
+    intids = get_utility(IIntIds)
+    parent_oid = int(request.params.get('parent'))
+    new_parent = intids.queryObject(parent_oid)
+    # check for changing parent
+    if request.params.get('action') == 'reparent':
+        child_oid = int(request.params.get('child'))
+        child = intids.queryObject(child_oid)
+        # check if new parent is not a previous child
+        parent_path = IPathElements(new_parent)
+        if child_oid in parent_path.parents:
+            return {
+                'status': 'reload',
+                'smallbox': {
+                    'status': 'error',
+                    'message': request.localizer.translate(_("Can't reparent object to one of it's children. "
+                                                             "Reloading...")),
+                    'timeout': 5000
+                }
+            }
+        old_parent = child.__parent__
+        new_name = old_name = child.__name__
+        if old_name in new_parent:
+            index = 1
+            new_name = '{name}-{index:02}'.format(name=old_name, index=index)
+            while new_name in new_parent:
+                index += 1
+                new_name = '{name}-{index:02}'.format(name=old_name, index=index)
+        new_parent[new_name] = child
+        del old_parent[old_name]
+        request.registry.notify(ObjectMovedEvent(child, old_parent, old_name, new_parent, new_name))
+    # Re-define order
+    if len(new_parent.keys()) > 1:
+        names = [child.__name__ for child in [intids.queryObject(oid)
+                                              for oid in map(int, json.loads(request.params.get('order')))]
+                 if (child is not None) and (child.__parent__ is new_parent)]
+        if names:
+            new_parent.updateOrder(names)
+    # get all new parent child
+    table = SiteContainerTreeTable(request.context, request,
+                                   can_sort=json.loads(request.params.get('can_sort', 'false')),
+                                   rows_state='plus')
+    table.update()
+    result = []
+    for item in new_parent.values():
+        row = table.setUpRow(item)
+        result.append(table.renderRow(row).strip())
+    return result
+
+
 @adapter_config(name='visible', context=(ISiteContainer, IPyAMSLayer, ISiteTreeTable), provides=IColumn)
 class SiteContainerTreeVisibleColumn(JsActionColumn):
     """Site container tree visible column"""
@@ -247,7 +298,8 @@
                   'td': 'action'}
 
     icon_class = 'fa fa-fw fa-eye'
-    icon_hint = _("Visible element?")
+    inactive_icon_hint = _("Visible element?")
+    active_icon_hint = _("Switch element visibility")
 
     url = 'PyAMS_content.site.switchVisibility'
     permission = MANAGE_CONTENT_PERMISSION
@@ -275,6 +327,13 @@
                 icon_class=icon_class,
                 title=self.request.localizer.translate(self.icon_hint))
 
+    def get_icon_hint(self, item):
+        translate = self.request.localizer.translate
+        if IContentLink.providedBy(item) and self.request.has_permission(self.permission, context=item):
+            return translate(self.active_icon_hint)
+        else:
+            return translate(self.inactive_icon_hint)
+
     def renderCell(self, item):
         if IContentLink.providedBy(item) and self.request.has_permission(self.permission, context=item):
             return super(SiteContainerTreeVisibleColumn, self).renderCell(item)
@@ -282,6 +341,21 @@
             return self.get_icon(item)
 
 
+@view_config(name='switch-content-visibility.json', context=ISiteContainer, request_type=IPyAMSLayer,
+             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+def switch_content_visibility(request):
+    """Switch content link visibility"""
+    container = ISiteContainer(request.context)
+    content = container.get(str(request.params.get('object_name')))
+    if not IContentLink.providedBy(content):
+        raise NotFound()
+    content.visible = not content.visible
+    return {
+        'visible': content.visible,
+        'published': IWorkflowPublicationInfo(content.__parent__).is_published()
+    }
+
+
 @adapter_config(name='name', context=(ISiteContainer, IPyAMSLayer, ISiteTreeTable), provides=IColumn)
 class SiteContainerTreeNameColumn(NameColumn):
     """Site container tree name column"""
@@ -415,6 +489,13 @@
         return super(SiteContainerTreeTrashColumn, self).has_permission(item) and item.is_deletable()
 
 
+@view_config(name='delete-element.json', context=ISiteContainer, request_type=IPyAMSLayer,
+             permission=MANAGE_SITE_PERMISSION, renderer='json', xhr=True)
+def delete_site_item(request):
+    """Delete item from site container"""
+    return delete_container_element(request, ignore_permission=True)
+
+
 @adapter_config(context=(ISiteContainer, IPyAMSLayer, ISiteTreeTable), provides=IValues)
 class SiteContainerTreeValuesAdapter(ContextRequestViewAdapter):
     """Site container tree values adapter"""
@@ -495,90 +576,6 @@
     return result
 
 
-@view_config(name='set-site-order.json', context=ISiteContainer, request_type=IPyAMSLayer,
-             permission=MANAGE_SITE_PERMISSION, renderer='json', xhr=True)
-def set_site_order(request):
-    """Set site elements order"""
-    intids = get_utility(IIntIds)
-    parent_oid = int(request.params.get('parent'))
-    new_parent = intids.queryObject(parent_oid)
-    # check for changing parent
-    if request.params.get('action') == 'reparent':
-        child_oid = int(request.params.get('child'))
-        child = intids.queryObject(child_oid)
-        # check if new parent is not a previous child
-        parent_path = IPathElements(new_parent)
-        if child_oid in parent_path.parents:
-            return {
-                'status': 'reload',
-                'smallbox': {
-                    'status': 'error',
-                    'message': request.localizer.translate(_("Can't reparent object to one of it's children. "
-                                                             "Reloading...")),
-                    'timeout': 5000
-                }
-            }
-        old_parent = child.__parent__
-        new_name = old_name = child.__name__
-        if old_name in new_parent:
-            index = 1
-            new_name = '{name}-{index:02}'.format(name=old_name, index=index)
-            while new_name in new_parent:
-                index += 1
-                new_name = '{name}-{index:02}'.format(name=old_name, index=index)
-        new_parent[new_name] = child
-        del old_parent[old_name]
-        request.registry.notify(ObjectMovedEvent(child, old_parent, old_name, new_parent, new_name))
-    # Re-define order
-    if len(new_parent.keys()) > 1:
-        names = [child.__name__ for child in [intids.queryObject(oid)
-                                              for oid in map(int, json.loads(request.params.get('order')))]
-                 if (child is not None) and (child.__parent__ is new_parent)]
-        if names:
-            new_parent.updateOrder(names)
-    # get all new parent child
-    table = SiteContainerTreeTable(request.context, request,
-                                   can_sort=json.loads(request.params.get('can_sort', 'false')),
-                                   rows_state='plus')
-    table.update()
-    result = []
-    for item in new_parent.values():
-        row = table.setUpRow(item)
-        result.append(table.renderRow(row).strip())
-    return result
-    
-
-@view_config(name='set-content-visibility.json', context=ISiteContainer, request_type=IPyAMSLayer,
-             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
-def set_content_visibility(request):
-    """Set content link visibility"""
-    container = ISiteContainer(request.context)
-    content = container.get(str(request.params.get('object_name')))
-    if not IContentLink.providedBy(content):
-        raise NotFound()
-    content.visible = not content.visible
-    return {'visible': content.visible,
-            'published': IWorkflowPublicationInfo(content.__parent__).is_published()}
-
-
-@view_config(name='delete-site-item.json', context=ISiteContainer, request_type=IPyAMSLayer,
-             permission=MANAGE_SITE_PERMISSION, renderer='json', xhr=True)
-def delete_site_item(request):
-    """Delete item from site container"""
-    translate = request.localizer.translate
-    name = request.params.get('object_name')
-    if not name:
-        return {'status': 'message',
-                'messagebox': {'status': 'error',
-                               'content': translate(_("No provided object_name argument!"))}}
-    if name not in request.context:
-        return {'status': 'message',
-                'messagebox': {'status': 'error',
-                               'content': translate(_("Given object name doesn't exist!"))}}
-    del request.context[name]
-    return {'status': 'success'}
-
-
 @adapter_config(context=(ISiteContainer, IPyAMSLayer, ISiteTreeTable), provides=ITableElementName)
 class SiteContainerTableElementName(ContextRequestViewAdapter):
     """Site container tree table element name"""
@@ -588,6 +585,14 @@
         return II18n(self.context).query_attribute('title', request=self.request)
 
 
+@adapter_config(context=(ISiteContainer, IPyAMSLayer, ISiteTreeTable), provides=ITableElementEditor)
+class SiteContainerTableElementEditor(DefaultElementEditorAdapter):
+    """Site container tree table element editor"""
+
+    view_name = 'admin#site-tree.html'
+    modal_target = False
+
+
 @adapter_config(context=(ISharedContent, IPyAMSLayer, ISiteTreeTable), provides=ITableElementName)
 class SharedContentTableElementName(ContextRequestViewAdapter):
     """Shared content tree table element name"""
@@ -598,14 +603,6 @@
         return II18n(version).query_attribute('title', request=self.request)
 
 
-@adapter_config(context=(ISiteContainer, IPyAMSLayer, ISiteTreeTable), provides=ITableElementEditor)
-class SiteContainerTableElementEditor(DefaultElementEditorAdapter):
-    """Site container tree table element editor"""
-
-    view_name = 'admin#site-tree.html'
-    modal_target = False
-
-
 @adapter_config(context=(ISharedContent, IPyAMSLayer, ISiteTreeTable), provides=ITableElementEditor)
 class SharedContentTableElementEditor(DefaultElementEditorAdapter):
     """Shared content tree table element editor"""
--- a/src/pyams_content/shared/site/zmi/link.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/shared/site/zmi/link.py	Wed Apr 11 16:46:31 2018 +0200
@@ -31,8 +31,7 @@
 
 # import packages
 from pyams_content.shared.site.link import ContentLink
-from pyams_content.shared.site.zmi.container import SiteContainerTreeTable, SiteContainerTreeNameColumn, \
-    SiteContainerTreeSequenceColumn
+from pyams_content.shared.site.zmi.container import SiteContainerTreeTable, SiteContainerTreeNameColumn
 from pyams_content.shared.site.zmi.widget import SiteManagerFoldersSelectorFieldWidget
 from pyams_form.form import AJAXAddForm, AJAXEditForm
 from pyams_pagelet.pagelet import pagelet_config
@@ -137,10 +136,11 @@
 class ContentLinkPropertiesEditForm(AdminDialogEditForm):
     """Content link properties edit form"""
 
+    prefix = 'link_properties.'
+
     legend = _("Edit content link properties")
 
     fields = field.Fields(IContentLink).omit('__parent__', '__name__')
-
     ajax_handler = 'properties.json'
     edit_permission = MANAGE_CONTENT_PERMISSION
 
--- a/src/pyams_content/shared/view/zmi/properties.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/shared/view/zmi/properties.py	Wed Apr 11 16:46:31 2018 +0200
@@ -35,6 +35,8 @@
 class ViewPropertiesEditForm(InnerAdminEditForm):
     """View properties edit form extension"""
 
+    prefix = 'view_settings.'
+
     legend = _("Main view settings")
     fieldset_class = 'bordered no-x-margin margin-y-10'
 
--- a/src/pyams_content/shared/view/zmi/reference.py	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/shared/view/zmi/reference.py	Wed Apr 11 16:46:31 2018 +0200
@@ -28,7 +28,6 @@
 
 # import packages
 from pyams_form.form import AJAXEditForm
-from pyams_form.schema import ResetButton
 from pyams_pagelet.pagelet import pagelet_config
 from pyams_skin.viewlet.menu import MenuItem, MenuDivider
 from pyams_viewlet.viewlet import viewlet_config
Binary file src/pyams_content/skin/resources/img/internal-link.png has changed
--- a/src/pyams_content/skin/resources/js/pyams_content.js	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/skin/resources/js/pyams_content.js	Wed Apr 11 16:46:31 2018 +0200
@@ -32,19 +32,73 @@
 		TinyMCE: {
 
 			initEditor: function(settings) {
+
+				// Update TinyMCE translations
+				tinyMCE.addI18n('fr', {
+					"Link list": "Liste de liens",
+					"Toggle h3 header": "En-tête H3",
+					"Toggle h4 header": "En-tête H4",
+					"Insert internal link": "Insérer un lien interne",
+					"Link title": "Texte à afficher",
+					"Internal number": "N° interne"
+				});
+
+				// Declare internal link input plug-in
+				tinymce.PluginManager.add('internal_links', function(editor, url) {
+					editor.addButton('internal_links', {
+						icon: 'cloud-check',
+						tooltip: "Insert internal link",
+						image: '/--static--/pyams_content/img/internal-link.png',
+						onclick: function() {
+							editor.windowManager.open({
+								title: "Insert internal link",
+								body: [
+									{
+										type: 'textbox',
+										name: 'oid',
+										label:'Internal number'
+									}, {
+										type: 'textbox',
+										name: 'title',
+										label: 'Link title',
+										value: editor.selection.getContent()
+									}
+								],
+								onsubmit: function(e) {
+									editor.insertContent('<a href="oid://' + e.data.oid + '">' + e.data.title + '</a>');
+								}
+							});
+						}
+					})
+				});
+
+				// Declare direct H3 and H4 style formatter
+				tinyMCE.PluginManager.add('headers', function (editor, url) {
+					['h3', 'h4'].forEach(function (name) {
+						editor.addButton("header-" + name, {
+							tooltip: "Toggle " + name + " header",
+							text: name.toUpperCase(),
+							onClick: function () {
+								editor.execCommand('mceToggleFormat', false, name);
+							},
+							onPostRender: function () {
+								var self = this,
+									setup = function () {
+										editor.formatter.formatChanged(name, function (state) {
+											self.active(state);
+										});
+									};
+								editor.formatter ? setup() : editor.on('init', setup);
+							}
+						})
+					});
+				});
+
+				// Update TinyMCE settings
 				settings.image_list = PyAMS_content.TinyMCE.getImagesList;
 				settings.link_list = PyAMS_content.TinyMCE.getLinksList;
 				settings.style_formats = [
 					{
-						title: 'Headings',
-						items: [
-							{title: 'Heading 3', format: 'h3'},
-							{title: 'Heading 4', format: 'h4'},
-							{title: 'Heading 5', format: 'h5'},
-							{title: 'Heading 6', format: 'h6'}
-						]
-					},
-					{
 						title: 'Inline',
 						items: [
 							{title: 'Bold', icon: 'bold', format: 'bold'},
@@ -75,29 +129,13 @@
 						]
 					}
 				];
-				// Declare direct H3 style formatter
-				tinyMCE.PluginManager.add('styles', function (editor, url) {
-					['h3'].forEach(function (name) {
-						editor.addButton("style-" + name, {
-							tooltip: "Toggle " + name + " header",
-							text: name.toUpperCase(),
-							onClick: function () {
-								editor.execCommand('mceToggleFormat', false, name);
-							},
-							onPostRender: function () {
-								var self = this, setup = function () {
-									editor.formatter.formatChanged(name, function (state) {
-										self.active(state);
-									});
-								};
-								editor.formatter ? setup() : editor.on('init', setup);
-							}
-						})
-					});
-				});
-				settings.plugins += ' styles';
+				settings.plugins += ' internal_links headers';
 				if (settings.toolbar1) {
-					settings.toolbar1 = "undo redo | styleselect style-h3 style-h4 | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent";
+					settings.toolbar1 = "undo redo | header-h3 header-h4 styleselect | bold italic | " +
+										"alignleft aligncenter alignright alignjustify | bullist numlist outdent indent";
+				}
+				if (settings.toolbar2) {
+					settings.toolbar2 = "forecolor backcolor | charmap internal_links link | fullscreen preview print | code";
 				}
 				return settings;
 			},
@@ -122,29 +160,6 @@
 		},
 
 		/**
-		 * Alerts management
-		 */
-		alerts: {
-
-			switchVisibility: function (element) {
-				return function () {
-					var source = $(this);
-					var association = source.parents('tr');
-					var container = association.parents('table');
-					MyAMS.ajax.post(container.data('ams-location') + '/set-alert-visibility.json',
-						{object_name: association.data('ams-element-name')},
-						function (result, status) {
-							if (result.visible) {
-								$('i', source).attr('class', 'fa fa-fw fa-eye');
-							} else {
-								$('i', source).attr('class', 'fa fa-fw fa-eye-slash text-danger');
-							}
-						});
-				};
-			}
-		},
-
-		/**
 		 * Galleries management
 		 */
 		galleries: {
@@ -228,39 +243,6 @@
 		},
 
 		/**
-		 * Associations management
-		 */
-		associations: {
-
-			refreshAssociations: function(options) {
-				// Reload widget
-				var widget = MyAMS.skin.refreshTable(options);
-				// Check fieldset state
-				var legend = widget.siblings('legend');
-				if (legend.parents('fieldset:first').hasClass('switched')) {
-					legend.click();
-				}
-			},
-
-			switchVisibility: function (element) {
-				return function () {
-					var source = $(this);
-					var association = source.parents('tr');
-					var container = association.parents('table');
-					MyAMS.ajax.post(container.data('ams-location') + '/set-association-visibility.json',
-						{object_name: association.data('ams-element-name')},
-						function (result, status) {
-							if (result.visible) {
-								$('i', source).attr('class', 'fa fa-fw fa-eye');
-							} else {
-								$('i', source).attr('class', 'fa fa-fw fa-eye-slash text-danger');
-							}
-						});
-				};
-			}
-		},
-
-		/**
 		 * Paragraphs management
 		 */
 		paragraphs: {
@@ -277,36 +259,8 @@
 				delete PyAMS_content.paragraphs.switched;
 			},
 
-			switchVisibility: function(element) {
-				return function() {
-					var source = $(this);
-					var para = source.parents('tr');
-					var container = para.parents('table');
-					MyAMS.ajax.post(container.data('ams-location') + '/set-paragraph-visibility.json',
-									{object_name: para.data('ams-element-name')},
-									function(result, status) {
-										if (result.visible) {
-											$('i', source).attr('class', 'fa fa-fw fa-eye');
-										} else {
-											$('i', source).attr('class', 'fa fa-fw fa-eye-slash text-danger');
-										}
-									});
-				};
-			},
-
-			refreshParagraphs: function(options) {
-				// Reload widget
-				var widget = MyAMS.skin.refreshTable(options);
-				// Check fieldset state
-				var legend = widget.siblings('legend');
-				if (legend.parents('fieldset:first').hasClass('switched')) {
-					legend.click();
-				}
-			},
-
 			refreshParagraph: function(changes) {
-				var container = $('table[id="paragraphs_list"]');
-				var para = $('tr[data-ams-element-name="' + changes.object_name + '"]', container);
+				var para = $('tr[id="' + changes.object_id + '"]');
 				$('span.title', para).html(changes.title || ' - - - - - - - -');
 			},
 
@@ -380,8 +334,7 @@
 			},
 
 			updateToolbar: function(settings) {
-				var container = $('table[id="paragraphs_list"]');
-				var para = $('tr[data-ams-element-name="' + settings.object_name + '"]', container);
+				var para = $('tr[id="' + settings.object_id + '"]');
 				var toolbar = $('.title-toolbar', para);
 				toolbar.replaceWith(settings.toolbar_tag);
 				toolbar = $('.title-toolbar', para);
@@ -389,8 +342,8 @@
 			},
 
 			updateMarkers: function(settings) {
-				var container = $('table[id="paragraphs_list"]');
-				var para = $('tr[data-ams-element-name="' + settings.object_name + '"]', container);
+				debugger;
+				var para = $('tr[id="' + settings.object_id + '"]');
 				var toolbar = $('.title-toolbar', para);
 				var marker = $('DIV.action.' + settings.marker_type, toolbar);
 				if (marker.exists()) {
@@ -407,39 +360,6 @@
 		},
 
 		/**
-		 * Milestones paragraph management
-		 */
-		milestones: {
-
-			refreshMilestones: function(options) {
-				// Reload widget
-				var widget = MyAMS.skin.refreshTable(options);
-				// Check fieldset state
-				var legend = widget.siblings('legend');
-				if (legend.parents('fieldset:first').hasClass('switched')) {
-					legend.click();
-				}
-			},
-
-			switchVisibility: function (element) {
-				return function () {
-					var source = $(this);
-					var milestone = source.parents('tr');
-					var container = milestone.parents('table');
-					MyAMS.ajax.post(container.data('ams-location') + '/set-milestone-visibility.json',
-						{object_name: milestone.data('ams-element-name')},
-						function (result, status) {
-							if (result.visible) {
-								$('i', source).attr('class', 'fa fa-fw fa-eye');
-							} else {
-								$('i', source).attr('class', 'fa fa-fw fa-eye-slash text-danger');
-							}
-						});
-				};
-			}
-		},
-
-		/**
 		 * Pictograms management
 		 */
 		pictograms: {
@@ -461,33 +381,6 @@
 				} else {
 					$('.available-pictograms', manager).append(pictogram);
 				}
-			},
-
-			refreshPictograms: function(options) {
-				// Reload widget
-				var widget = MyAMS.skin.refreshTable(options);
-				// Check fieldset state
-				var legend = widget.siblings('legend');
-				if (legend.parents('fieldset:first').hasClass('switched')) {
-					legend.click();
-				}
-			},
-
-			switchVisibility: function (element) {
-				return function () {
-					var source = $(this);
-					var pictogram = source.parents('tr');
-					var container = pictogram.parents('table');
-					MyAMS.ajax.post(container.data('ams-location') + '/set-pictogram-visibility.json',
-						{object_name: pictogram.data('ams-element-name')},
-						function (result, status) {
-							if (result.visible) {
-								$('i', source).attr('class', 'fa fa-fw fa-eye');
-							} else {
-								$('i', source).attr('class', 'fa fa-fw fa-eye-slash text-danger');
-							}
-						});
-				};
 			}
 		},
 
@@ -546,23 +439,6 @@
 				var container = $('table[id="form_fields_list"]');
 				var para = $('tr[data-ams-element-name="' + changes.object_name + '"]', container);
 				$('td:nth-child(4)', para).html(changes.title);
-			},
-
-			switchVisibility: function (element) {
-				return function () {
-					var source = $(this);
-					var association = source.parents('tr');
-					var container = association.parents('table');
-					MyAMS.ajax.post(container.data('ams-location') + '/set-form-field-visibility.json',
-						{object_name: association.data('ams-element-name')},
-						function (result, status) {
-							if (result.visible) {
-								$('i', source).attr('class', 'fa fa-fw fa-eye');
-							} else {
-								$('i', source).attr('class', 'fa fa-fw fa-eye-slash text-danger');
-							}
-						});
-				};
 			}
 		},
 
@@ -638,14 +514,12 @@
 				return function() {
 					var source = $(this);
 					var content = source.parents('tr').first();
-					MyAMS.ajax.post(content.data('ams-location') + '/set-content-visibility.json',
+					MyAMS.ajax.post(content.data('ams-location') + '/switch-content-visibility.json',
 									{object_name: content.data('ams-element-name')},
 									function(result, status) {
-										var klass;
-										if (result.visible) {
-											klass = 'fa-eye';
-										} else {
-											klass = 'fa-eye-slash';
+										var klass = 'fa-eye';
+										if (!result.visible) {
+											klass += '-slash';
 										}
 										if (!result.published) {
 											klass += ' text-danger';
--- a/src/pyams_content/skin/resources/js/pyams_content.min.js	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/skin/resources/js/pyams_content.min.js	Wed Apr 11 16:46:31 2018 +0200
@@ -1,1 +1,1 @@
-!function(a,t){"use strict";var e=t.MyAMS,s={widget:{treeview:{selectFolder:function(t,e){a(t.target).siblings('input[type="hidden"]').val(e.id)},unselectFolder:function(t,e){a(t.target).siblings('input[type="hidden"]').val(null)}}},TinyMCE:{initEditor:function(a){return a.image_list=s.TinyMCE.getImagesList,a.link_list=s.TinyMCE.getLinksList,a},getImagesList:function(t){var s=a(document.activeElement).parents("form");if(s.exists()){var i=s.attr("data-ams-form-handler")||s.attr("action"),n=i.substr(0,i.lastIndexOf("/")+1);return e.ajax.post(n+"get-images-list.json",{},t)}},getLinksList:function(t){var s=a(document.activeElement).parents("form");if(s.exists()){var i=s.attr("data-ams-form-handler")||s.attr("action"),n=i.substr(0,i.lastIndexOf("/")+1);return e.ajax.post(n+"get-links-list.json",{},t)}}},alerts:{switchVisibility:function(t){return function(){var t=a(this),s=t.parents("tr"),i=s.parents("table");e.ajax.post(i.data("ams-location")+"/set-alert-visibility.json",{object_name:s.data("ams-element-name")},function(e,s){e.visible?a("i",t).attr("class","fa fa-fw fa-eye"):a("i",t).attr("class","fa fa-fw fa-eye-slash text-danger")})}}},galleries:{updateMediaTitle:function(t){a('img[id="'+t.media_id+'"]').attr("original-title",t.title)},switchMediaVisibility:function(t){return function(){var t=a(this),s=t.parents(".media"),i=s.parents(".gallery");e.ajax.post(i.data("ams-location")+"/set-media-visibility.json",{object_name:s.data("ams-element-name")},function(e,s){e.visible?(a("i",t).attr("class","fa fa-fw fa-eye"),t.parents(".btn-group").siblings("a.fancyimg").removeClass("not-visible")):(a("i",t).attr("class","fa fa-fw fa-eye-slash text-danger"),t.parents(".btn-group").siblings("a.fancyimg").addClass("not-visible"))})}},setOrder:function(t,s){if(!s||!s.item.hasClass("already-dropped")){var i=s.item.parents(".gallery"),n=a(".media",i).listattr("data-ams-element-name");e.ajax.post(i.data("ams-location")+"/set-medias-order.json",{medias:JSON.stringify(n)})}},removeMedia:function(t){return function(){var t=a(this);e.skin.bigBox({title:e.i18n.WARNING,content:'<i class="text-danger fa fa-2x fa-bell shake animated"></i>&nbsp; '+e.i18n.DELETE_WARNING,buttons:e.i18n.BTN_OK_CANCEL},function(a){if(a===e.i18n.BTN_OK){var s=t.parents(".gallery").data("ams-location"),i=t.parents(".media"),n=i.data("ams-element-name");e.ajax.post(s+"/delete-element.json",{object_name:n},function(a,t){i.remove()})}})}},afterFancyboxLoad:function(a,t){a.element.hasClass("not-visible")&&a.inner.prepend('<div class="hidden-mask"></div>')}},illustration:{addIllustration:function(){var t=a(this),e=t.parents(".btn-group").siblings("legend.switcher");a("i.fa-plus",e).click(),t.hide()}},associations:{refreshAssociations:function(a){var t=e.skin.refreshTable(a).siblings("legend");t.parents("fieldset:first").hasClass("switched")&&t.click()},switchVisibility:function(t){return function(){var t=a(this),s=t.parents("tr"),i=s.parents("table");e.ajax.post(i.data("ams-location")+"/set-association-visibility.json",{object_name:s.data("ams-element-name")},function(e,s){e.visible?a("i",t).attr("class","fa fa-fw fa-eye"):a("i",t).attr("class","fa fa-fw fa-eye-slash text-danger")})}}},paragraphs:{preReload:function(){s.paragraphs.switched=a("i.switch.fa-minus-square-o","#paragraphs_list").parents("tr").listattr("id")},postReload:function(){a(s.paragraphs.switched).each(function(){a("i.switch.fa-plus-square-o",'[id="'+this+'"]').parents("div").first().click()}),delete s.paragraphs.switched},switchVisibility:function(t){return function(){var t=a(this),s=t.parents("tr"),i=s.parents("table");e.ajax.post(i.data("ams-location")+"/set-paragraph-visibility.json",{object_name:s.data("ams-element-name")},function(e,s){e.visible?a("i",t).attr("class","fa fa-fw fa-eye"):a("i",t).attr("class","fa fa-fw fa-eye-slash text-danger")})}},refreshParagraphs:function(a){var t=e.skin.refreshTable(a).siblings("legend");t.parents("fieldset:first").hasClass("switched")&&t.click()},refreshParagraph:function(t){var e=a('table[id="paragraphs_list"]'),s=a('tr[data-ams-element-name="'+t.object_name+'"]',e);a("span.title",s).html(t.title||" - - - - - - - -")},switchEditor:function(t){var s=a(this),i=a("i.switch",s),n=s.parents("td"),r=a(".editor",n),l=s.parents("tr");if(i.hasClass("fa-plus-square-o")){var o=l.parents("table");r.html('<h1 class="loading"><i class="fa fa-2x fa-gear fa-spin"></i></h1>'),e.ajax.post(o.data("ams-location")+"/get-paragraph-editor.json",{object_name:l.data("ams-element-name")},function(a){r.html(a),a&&(e.initContent(r),i.removeClass("fa-plus-square-o").addClass("fa-minus-square-o"),l.data("ams-disabled-handlers",!0))})}else e.skin.cleanContainer(r),r.empty(),i.removeClass("fa-minus-square-o").addClass("fa-plus-square-o"),l.removeData("ams-disabled-handlers")},switchAllEditors:function(t){var s=a(this),i=a("i",s),n=s.parents("table");i.hasClass("fa-plus-square-o")?(i.removeClass("fa-plus-square-o").addClass("fa-cog fa-spin"),e.ajax.post(n.data("ams-location")+"/get-paragraphs-editors.json",{},function(t){for(var s in t)if(t.hasOwnProperty(s)){var r=a('tr[data-ams-element-name="'+s+'"]',n),l=a(".editor",r);l.is(":empty")&&(l.html(t[s]),e.initContent(l)),a(".fa-plus-square-o",r).removeClass("fa-plus-square-o").addClass("fa-minus-square-o"),r.data("ams-disabled-handlers",!0)}a("i.fa-plus-square-o",a("tbody",n)).exists()||i.removeClass("fa-cog fa-spin").addClass("fa-minus-square-o")})):(a(".editor",n).each(function(){e.skin.cleanContainer(a(this)),a(this).empty()}),a(".fa-minus-square-o",n).removeClass("fa-minus-square-o").addClass("fa-plus-square-o"),a("tr",n).removeData("ams-disabled-handlers"))},updateToolbar:function(t){var s=a('table[id="paragraphs_list"]'),i=a('tr[data-ams-element-name="'+t.object_name+'"]',s),n=a(".title-toolbar",i);n.replaceWith(t.toolbar_tag),n=a(".title-toolbar",i),e.initContent(n)},updateMarkers:function(t){var s=a('table[id="paragraphs_list"]'),i=a('tr[data-ams-element-name="'+t.object_name+'"]',s),n=a(".title-toolbar",i),r=a("DIV.action."+t.marker_type,n);r.exists()?r.replaceWith(t.marker_tag):a(t.marker_tag).appendTo(n),t.marker_tag&&(r=a("DIV.action."+t.marker_type,n),e.initContent(r)),e.helpers.sort(n,"weight")}},milestones:{refreshMilestones:function(a){var t=e.skin.refreshTable(a).siblings("legend");t.parents("fieldset:first").hasClass("switched")&&t.click()},switchVisibility:function(t){return function(){var t=a(this),s=t.parents("tr"),i=s.parents("table");e.ajax.post(i.data("ams-location")+"/set-milestone-visibility.json",{object_name:s.data("ams-element-name")},function(e,s){e.visible?a("i",t).attr("class","fa fa-fw fa-eye"):a("i",t).attr("class","fa fa-fw fa-eye-slash text-danger")})}}},pictograms:{initManagerSelection:function(){var t=a(this),e=a('input[type="hidden"]',a(".selected-pictograms",t)).listattr("value");return{selected:JSON.stringify(e)}},switchPictogram:function(){var t=a(this),e=t.parents(".pictograms"),s=e.parents(".pictograms-manager");e.hasClass("available-pictograms")?a(".selected-pictograms",s).append(t):a(".available-pictograms",s).append(t)},refreshPictograms:function(a){var t=e.skin.refreshTable(a).siblings("legend");t.parents("fieldset:first").hasClass("switched")&&t.click()},switchVisibility:function(t){return function(){var t=a(this),s=t.parents("tr"),i=s.parents("table");e.ajax.post(i.data("ams-location")+"/set-pictogram-visibility.json",{object_name:s.data("ams-element-name")},function(e,s){e.visible?a("i",t).attr("class","fa fa-fw fa-eye"):a("i",t).attr("class","fa fa-fw fa-eye-slash text-danger")})}}},themes:{initExtracts:function(t){var s=a('select[name="form.widgets.thesaurus_name:list"]',t).val(),i=a('select[name="form.widgets.extract_name:list"]',t),n=i.val();s&&e.jsonrpc.post("getExtracts",{thesaurus_name:s},{url:"/api/thesaurus/json"},function(t){i.empty(),a(t.result).each(function(){a("<option></option>").attr("value",this.id).attr("selected",this.id===n).text(this.text).appendTo(i)})}),i.attr("data-ams-events-handlers",'{"select2-open": "PyAMS_content.themes.getExtracts"}')},getExtracts:function(t){var s=a(t.currentTarget).parents("form"),i=a('select[name="form.widgets.thesaurus_name:list"]',s).val();i&&e.jsonrpc.post("getExtracts",{thesaurus_name:i},{url:"/api/thesaurus/json"},function(t){var e=a('select[name="form.widgets.extract_name:list"]',s).data("select2");e.results.empty(),e.opts.populateResults.call(e,e.results,t.result,{term:""})})}},fields:{refreshField:function(t){var e=a('table[id="form_fields_list"]'),s=a('tr[data-ams-element-name="'+t.object_name+'"]',e);a("td:nth-child(4)",s).html(t.title)},switchVisibility:function(t){return function(){var t=a(this),s=t.parents("tr"),i=s.parents("table");e.ajax.post(i.data("ams-location")+"/set-form-field-visibility.json",{object_name:s.data("ams-element-name")},function(e,s){e.visible?a("i",t).attr("class","fa fa-fw fa-eye"):a("i",t).attr("class","fa fa-fw fa-eye-slash text-danger")})}}},imgmap:{init:function(){var t=a(this);e.ajax.check(a.fn.canvasAreaDraw,"/--static--/pyams_content/js/jquery-canvasAreaDraw"+e.devext+".js",function(){t.canvasAreaDraw({imageUrl:t.data("ams-image-url")})})},initPreview:function(){var t=a(this);e.ajax.check(a.fn.mapster,"/--static--/pyams_content/js/jquery-imagemapster-1.2.10"+e.devext+".js",function(){t.mapster({fillColor:"ff0000",fillOpacity:.35,selected:!0,highlight:!0,staticState:!0})})}},types:{switchSubtypes:function(t){var s=a(this),i=a("i.switch",s),n=s.parents("td"),r=a(".subtypes",n),l=s.parents("tr");if(i.hasClass("fa-plus-square-o")){var o=l.parents("table");r.html('<h1 class="loading"><i class="fa fa-2x fa-gear fa-spin"></i></h1>'),e.ajax.post(o.data("ams-location")+"/get-subtypes-table.json",{object_name:l.data("ams-element-name")},function(a){r.html(a),a&&(e.initContent(r),i.removeClass("fa-plus-square-o").addClass("fa-minus-square-o"))})}else e.skin.cleanContainer(r),r.empty(),i.removeClass("fa-minus-square-o").addClass("fa-plus-square-o")}},site:{switchVisibility:function(){return function(){var t=a(this),s=t.parents("tr").first();e.ajax.post(s.data("ams-location")+"/set-content-visibility.json",{object_name:s.data("ams-element-name")},function(e,s){var i;i=e.visible?"fa-eye":"fa-eye-slash",e.published||(i+=" text-danger"),a("i",t).attr("class","fa fa-fw "+i)})}}},review:{timer:null,timer_duration:{general:3e4,chat:5e3},initComments:function(t){var i=a(".chat-body",t);i.animate({scrollTop:i[0].scrollHeight},1e3),clearInterval(s.review.timer),s.review.timer=setInterval(s.review.updateComments,s.review.timer_duration.chat),e.skin.registerCleanCallback(s.review.cleanCommentsCallback)},cleanCommentsCallback:function(){clearInterval(s.review.timer),s.review.timer=setInterval(s.review.updateComments,s.review.timer_duration.general)},updateComments:function(){var t,s=a(".badge",'nav a[href="#review-comments.html"]'),i=a(".chat-body",".widget-body");t=i.exists()?a(".message",i).length:parseInt(s.text()),e.ajax.post("get-last-review-comments.json",{count:t},function(e){i.exists()&&s.removeClass("bg-color-danger").addClass("bg-color-info"),t!==e.count&&(s.text(e.count).removeClass("hidden"),i.exists()&&(a(".messages",i).append(e.content),i.animate({scrollTop:i[0].scrollHeight},1e3)),i.exists()||s.removeClass("bg-color-info").addClass("bg-color-danger").animate({padding:"3px 12px 2px","margin-right":"9px"},"slow",function(){a(this).animate({padding:"3px 6px 2px","margin-right":"15px"},"slow")}))})},initCommentData:function(t){var e=a(".chat-body",".widget-body");return{count:a(".message",e).length}},addCommentAction:function(){return function(){a('textarea[name="comment"]').focus()}},addCommentCallback:function(t){var e=a(this),s=e.parents(".widget-body");a(".messages",s).append(t.content),a('textarea[name="comment"]',e).val("");var i=a(".chat-body",s);i.animate({scrollTop:i[0].scrollHeight},1e3),a(".badge",'nav a[href="#review-comments.html"]').text(t.count).removeClass("hidden")}},profile:{switchFavorite:function(){var t=a(this),s=t.data("sequence-oid");e.ajax.post("switch-user-favorite.json",{oid:s},function(a,e){a.favorite?t.removeClass("fa-star-o").addClass("fa-star"):t.removeClass("fa-star").addClass("fa-star-o")})}}};a(".badge",'nav a[href="#review-comments.html"]').exists()&&(s.review.timer=setInterval(s.review.updateComments,s.review.timer_duration.general)),t.PyAMS_content=s}(jQuery,this);
+!function(t,e){"use strict";var a=e.MyAMS,i={widget:{treeview:{selectFolder:function(e,a){t(e.target).siblings('input[type="hidden"]').val(a.id)},unselectFolder:function(e,a){t(e.target).siblings('input[type="hidden"]').val(null)}}},TinyMCE:{initEditor:function(t){return tinyMCE.addI18n("fr",{"Link list":"Liste de liens","Toggle h3 header":"En-tête H3","Toggle h4 header":"En-tête H4","Insert internal link":"Insérer un lien interne","Link title":"Texte du lien","Internal number":"N° interne"}),tinymce.PluginManager.add("internal_links",function(t,e){t.addButton("internal_links",{icon:"cloud-check",tooltip:"Insert internal link",image:"/--static--/pyams_content/img/internal-link.png",onclick:function(){t.windowManager.open({title:"Insert internal link",body:[{type:"textbox",name:"title",label:"Link title",value:t.selection.getContent()},{type:"textbox",name:"oid",label:"Internal number"}],onsubmit:function(e){t.insertContent('<a href="oid://'+e.data.oid+'">'+e.data.title+"</a>")}})}})}),tinyMCE.PluginManager.add("headers",function(t,e){["h3","h4"].forEach(function(e){t.addButton("header-"+e,{tooltip:"Toggle "+e+" header",text:e.toUpperCase(),onClick:function(){t.execCommand("mceToggleFormat",!1,e)},onPostRender:function(){var a=this,i=function(){t.formatter.formatChanged(e,function(t){a.active(t)})};t.formatter?i():t.on("init",i)}})})}),t.image_list=i.TinyMCE.getImagesList,t.link_list=i.TinyMCE.getLinksList,t.style_formats=[{title:"Inline",items:[{title:"Bold",icon:"bold",format:"bold"},{title:"Italic",icon:"italic",format:"italic"},{title:"Underline",icon:"underline",format:"underline"},{title:"Strikethrough",icon:"strikethrough",format:"strikethrough"},{title:"Superscript",icon:"superscript",format:"superscript"},{title:"Subscript",icon:"subscript",format:"subscript"},{title:"Code",icon:"code",format:"code"}]},{title:"Blocks",items:[{title:"Paragraph",format:"p"},{title:"Blockquote",format:"blockquote"},{title:"Div",format:"div"},{title:"Pre",format:"pre"}]},{title:"Alignment",items:[{title:"Left",icon:"alignleft",format:"alignleft"},{title:"Center",icon:"aligncenter",format:"aligncenter"},{title:"Right",icon:"alignright",format:"alignright"},{title:"Justify",icon:"alignjustify",format:"alignjustify"}]}],t.plugins+=" internal_links headers",t.toolbar1&&(t.toolbar1="undo redo | header-h3 header-h4 styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent"),t.toolbar2&&(t.toolbar2="forecolor backcolor | charmap internal_links link | fullscreen preview print | code"),t},getImagesList:function(e){var i=t(document.activeElement).parents("form");if(i.exists()){var n=i.attr("data-ams-form-handler")||i.attr("action"),s=n.substr(0,n.lastIndexOf("/")+1);return a.ajax.post(s+"get-images-list.json",{},e)}},getLinksList:function(e){var i=t(document.activeElement).parents("form");if(i.exists()){var n=i.attr("data-ams-form-handler")||i.attr("action"),s=n.substr(0,n.lastIndexOf("/")+1);return a.ajax.post(s+"get-links-list.json",{},e)}}},galleries:{updateMediaTitle:function(e){t('img[id="'+e.media_id+'"]').attr("original-title",e.title)},switchMediaVisibility:function(e){return function(){var e=t(this),i=e.parents(".media"),n=i.parents(".gallery");a.ajax.post(n.data("ams-location")+"/set-media-visibility.json",{object_name:i.data("ams-element-name")},function(a,i){a.visible?(t("i",e).attr("class","fa fa-fw fa-eye"),e.parents(".btn-group").siblings("a.fancyimg").removeClass("not-visible")):(t("i",e).attr("class","fa fa-fw fa-eye-slash text-danger"),e.parents(".btn-group").siblings("a.fancyimg").addClass("not-visible"))})}},setOrder:function(e,i){if(!i||!i.item.hasClass("already-dropped")){var n=i.item.parents(".gallery"),s=t(".media",n).listattr("data-ams-element-name");a.ajax.post(n.data("ams-location")+"/set-medias-order.json",{medias:JSON.stringify(s)})}},removeMedia:function(e){return function(){var e=t(this);a.skin.bigBox({title:a.i18n.WARNING,content:'<i class="text-danger fa fa-2x fa-bell shake animated"></i>&nbsp; '+a.i18n.DELETE_WARNING,buttons:a.i18n.BTN_OK_CANCEL},function(t){if(t===a.i18n.BTN_OK){var i=e.parents(".gallery").data("ams-location"),n=e.parents(".media"),s=n.data("ams-element-name");a.ajax.post(i+"/delete-element.json",{object_name:s},function(t,e){n.remove()})}})}},afterFancyboxLoad:function(t,e){t.element.hasClass("not-visible")&&t.inner.prepend('<div class="hidden-mask"></div>')}},illustration:{addIllustration:function(){var e=t(this),a=e.parents(".btn-group").siblings("legend.switcher");t("i.fa-plus",a).click(),e.hide()}},paragraphs:{preReload:function(){i.paragraphs.switched=t("i.switch.fa-minus-square-o","#paragraphs_list").parents("tr").listattr("id")},postReload:function(){t(i.paragraphs.switched).each(function(){t("i.switch.fa-plus-square-o",'[id="'+this+'"]').parents("div").first().click()}),delete i.paragraphs.switched},refreshParagraph:function(e){var a=t('table[id="paragraphs_list"]'),i=t('tr[data-ams-element-name="'+e.object_name+'"]',a);t("span.title",i).html(e.title||" - - - - - - - -")},switchEditor:function(e){var i=t(this),n=t("i.switch",i),s=i.parents("td"),r=t(".editor",s),o=i.parents("tr");if(n.hasClass("fa-plus-square-o")){var l=o.parents("table");r.html('<h1 class="loading"><i class="fa fa-2x fa-gear fa-spin"></i></h1>'),a.ajax.post(l.data("ams-location")+"/get-paragraph-editor.json",{object_name:o.data("ams-element-name")},function(t){r.html(t),t&&(a.initContent(r),n.removeClass("fa-plus-square-o").addClass("fa-minus-square-o"),o.data("ams-disabled-handlers",!0))})}else a.skin.cleanContainer(r),r.empty(),n.removeClass("fa-minus-square-o").addClass("fa-plus-square-o"),o.removeData("ams-disabled-handlers")},switchAllEditors:function(e){var i=t(this),n=t("i",i),s=i.parents("table");n.hasClass("fa-plus-square-o")?(n.removeClass("fa-plus-square-o").addClass("fa-cog fa-spin"),a.ajax.post(s.data("ams-location")+"/get-paragraphs-editors.json",{},function(e){for(var i in e)if(e.hasOwnProperty(i)){var r=t('tr[data-ams-element-name="'+i+'"]',s),o=t(".editor",r);o.is(":empty")&&o.html(e[i]),t(".fa-plus-square-o",r).removeClass("fa-plus-square-o").addClass("fa-minus-square-o"),r.data("ams-disabled-handlers",!0)}t("i.fa-plus-square-o",t("tbody",s)).exists()||n.removeClass("fa-cog fa-spin").addClass("fa-minus-square-o"),a.initContent(s)})):(t(".editor",s).each(function(){a.skin.cleanContainer(t(this)),t(this).empty()}),t(".fa-minus-square-o",s).removeClass("fa-minus-square-o").addClass("fa-plus-square-o"),t("tr",s).removeData("ams-disabled-handlers"))},updateToolbar:function(e){var i=t('table[id="paragraphs_list"]'),n=t('tr[data-ams-element-name="'+e.object_name+'"]',i),s=t(".title-toolbar",n);s.replaceWith(e.toolbar_tag),s=t(".title-toolbar",n),a.initContent(s)},updateMarkers:function(e){var i=t('table[id="paragraphs_list"]'),n=t('tr[data-ams-element-name="'+e.object_name+'"]',i),s=t(".title-toolbar",n),r=t("DIV.action."+e.marker_type,s);r.exists()?r.replaceWith(e.marker_tag):t(e.marker_tag).appendTo(s),e.marker_tag&&(r=t("DIV.action."+e.marker_type,s),a.initContent(r)),a.helpers.sort(s,"weight")}},pictograms:{initManagerSelection:function(){var e=t(this),a=t('input[type="hidden"]',t(".selected-pictograms",e)).listattr("value");return{selected:JSON.stringify(a)}},switchPictogram:function(){var e=t(this),a=e.parents(".pictograms"),i=a.parents(".pictograms-manager");a.hasClass("available-pictograms")?t(".selected-pictograms",i).append(e):t(".available-pictograms",i).append(e)}},themes:{initExtracts:function(e){var i=t('select[name="form.widgets.thesaurus_name:list"]',e).val(),n=t('select[name="form.widgets.extract_name:list"]',e),s=n.val();i&&a.jsonrpc.post("getExtracts",{thesaurus_name:i},{url:"/api/thesaurus/json"},function(e){n.empty(),t(e.result).each(function(){t("<option></option>").attr("value",this.id).attr("selected",this.id===s).text(this.text).appendTo(n)})}),n.attr("data-ams-events-handlers",'{"select2-open": "PyAMS_content.themes.getExtracts"}')},getExtracts:function(e){var i=t(e.currentTarget).parents("form"),n=t('select[name="form.widgets.thesaurus_name:list"]',i).val();n&&a.jsonrpc.post("getExtracts",{thesaurus_name:n},{url:"/api/thesaurus/json"},function(e){var a=t('select[name="form.widgets.extract_name:list"]',i).data("select2");a.results.empty(),a.opts.populateResults.call(a,a.results,e.result,{term:""})})}},fields:{refreshField:function(e){var a=t('table[id="form_fields_list"]'),i=t('tr[data-ams-element-name="'+e.object_name+'"]',a);t("td:nth-child(4)",i).html(e.title)}},imgmap:{init:function(){var e=t(this);a.ajax.check(t.fn.canvasAreaDraw,"/--static--/pyams_content/js/jquery-canvasAreaDraw"+a.devext+".js",function(){e.canvasAreaDraw({imageUrl:e.data("ams-image-url")})})},initPreview:function(){var e=t(this);a.ajax.check(t.fn.mapster,"/--static--/pyams_content/js/jquery-imagemapster-1.2.10"+a.devext+".js",function(){e.mapster({fillColor:"ff0000",fillOpacity:.35,selected:!0,highlight:!0,staticState:!0})})}},types:{switchSubtypes:function(e){var i=t(this),n=t("i.switch",i),s=i.parents("td"),r=t(".subtypes",s),o=i.parents("tr");if(n.hasClass("fa-plus-square-o")){var l=o.parents("table");r.html('<h1 class="loading"><i class="fa fa-2x fa-gear fa-spin"></i></h1>'),a.ajax.post(l.data("ams-location")+"/get-subtypes-table.json",{object_name:o.data("ams-element-name")},function(t){r.html(t),t&&(a.initContent(r),n.removeClass("fa-plus-square-o").addClass("fa-minus-square-o"))})}else a.skin.cleanContainer(r),r.empty(),n.removeClass("fa-minus-square-o").addClass("fa-plus-square-o")}},site:{switchVisibility:function(){return function(){var e=t(this),i=e.parents("tr").first();a.ajax.post(i.data("ams-location")+"/switch-content-visibility.json",{object_name:i.data("ams-element-name")},function(a,i){var n="fa-eye";a.visible||(n+="-slash"),a.published||(n+=" text-danger"),t("i",e).attr("class","fa fa-fw "+n)})}}},review:{timer:null,timer_duration:{general:3e4,chat:5e3},initComments:function(e){var n=t(".chat-body",e);n.animate({scrollTop:n[0].scrollHeight},1e3),clearInterval(i.review.timer),i.review.timer=setInterval(i.review.updateComments,i.review.timer_duration.chat),a.skin.registerCleanCallback(i.review.cleanCommentsCallback)},cleanCommentsCallback:function(){clearInterval(i.review.timer),i.review.timer=setInterval(i.review.updateComments,i.review.timer_duration.general)},updateComments:function(){var e,i=t(".badge",'nav a[href="#review-comments.html"]'),n=t(".chat-body",".widget-body");e=n.exists()?t(".message",n).length:parseInt(i.text()),a.ajax.post("get-last-review-comments.json",{count:e},function(a){n.exists()&&i.removeClass("bg-color-danger").addClass("bg-color-info"),e!==a.count&&(i.text(a.count).removeClass("hidden"),n.exists()&&(t(".messages",n).append(a.content),n.animate({scrollTop:n[0].scrollHeight},1e3)),n.exists()||i.removeClass("bg-color-info").addClass("bg-color-danger").animate({padding:"3px 12px 2px","margin-right":"9px"},"slow",function(){t(this).animate({padding:"3px 6px 2px","margin-right":"15px"},"slow")}))})},initCommentData:function(e){var a=t(".chat-body",".widget-body");return{count:t(".message",a).length}},addCommentAction:function(){return function(){t('textarea[name="comment"]').focus()}},addCommentCallback:function(e){var a=t(this),i=a.parents(".widget-body");t(".messages",i).append(e.content),t('textarea[name="comment"]',a).val("");var n=t(".chat-body",i);n.animate({scrollTop:n[0].scrollHeight},1e3),t(".badge",'nav a[href="#review-comments.html"]').text(e.count).removeClass("hidden")}},profile:{switchFavorite:function(){var e=t(this),i=e.data("sequence-oid");a.ajax.post("switch-user-favorite.json",{oid:i},function(t,a){t.favorite?e.removeClass("fa-star-o").addClass("fa-star"):e.removeClass("fa-star").addClass("fa-star-o")})}}};t(".badge",'nav a[href="#review-comments.html"]').exists()&&(i.review.timer=setInterval(i.review.updateComments,i.review.timer_duration.general)),e.PyAMS_content=i}(jQuery,this);
--- a/src/pyams_content/skin/resources/js/tinymce/onflinks/plugin.js	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/skin/resources/js/tinymce/onflinks/plugin.js	Wed Apr 11 16:46:31 2018 +0200
@@ -1,6 +1,6 @@
-tinymce.PluginManager.add('onflinks', function(editor, url) {
+tinymce.PluginManager.add('internal_links', function(editor, url) {
 
-	editor.addButton('onflinks', {
+	editor.addButton('internal_links', {
 		icon: 'cloud-check',
 		tooltip: "Insert internal link",
 		image: '/--static--/pyams_content/img/external.png',
--- a/src/pyams_content/skin/resources/js/tinymce/onflinks/plugin.min.js	Wed Apr 11 16:44:46 2018 +0200
+++ b/src/pyams_content/skin/resources/js/tinymce/onflinks/plugin.min.js	Wed Apr 11 16:46:31 2018 +0200
@@ -1,1 +1,1 @@
-tinymce.PluginManager.add("onflinks",function(b,a){b.addButton("onflinks",{icon:"cloud-check",tooltip:"Insert internal link",image:"/--static--/onf_website/img/external.png",onclick:function(){b.windowManager.open({title:"Insert internal link",body:[{type:"textbox",name:"title",label:"Link title",value:b.selection.getContent()},{type:"textbox",name:"oid",label:"Internal number"}],onsubmit:function(c){b.insertContent('<a href="oid://'+c.data.oid+'">'+c.data.title+"</a>")}})}})});
\ No newline at end of file
+tinymce.PluginManager.add("internal_links",function(n,t){n.addButton("internal_links",{icon:"cloud-check",tooltip:"Insert internal link",image:"/--static--/pyams_content/img/external.png",onclick:function(){n.windowManager.open({title:"Insert internal link",body:[{type:"textbox",name:"title",label:"Link title",value:n.selection.getContent()},{type:"textbox",name:"oid",label:"Internal number"}],onsubmit:function(t){n.insertContent('<a href="oid://'+t.data.oid+'">'+t.data.title+"</a>")}})}})});