Skip to content

XWIKI-23129: Add an end of DOM UIXP #4085

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@
// Prevent Chrome on Windows from adding a focus outline. For details, see
// https://github.com/twbs/bootstrap/pull/10951.
outline: 0;

// If this element is a dialog, we want to reset those default styles. The children elements are the ones painted.
dialog& {
background-color: transparent;
border: 0;
}

// When fading in the modal, animate it to slide down
&.fade .modal-dialog {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
## Display UI Extensions before the title element
## --------------------------------------------------------
#foreach ($uix in $services.uix.getExtensions('org.xwiki.platform.template.title.before'))
$services.rendering.render($uix.execute(), 'xhtml/1.0')
$services.rendering.render($uix.execute(), 'html/5.0')
#end
<div id="document-title"><h1>$titleToDisplay</h1></div>
#if (!$doc.isNew())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
*#
#macro(exportModal)
#set ($discard = $xwiki.jsfx.use('uicomponents/exporter/exporter.js', {'forceSkinAction': true}))
<div class="modal fade text-left" id="exportModal" tabindex="-1" role="dialog" aria-labelledby="exportModalLabel"
<dialog class="modal fade text-left" id="exportModal" aria-labelledby="exportModalLabel"
aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
Expand All @@ -46,7 +46,7 @@
</div>
</div>
</div>
</div>
</dialog>
#if ($doc.documentReference.name == 'WebHome' && $xwiki.exists('XWiki.ExportDocumentTree'))
#exportTreeModal()
#end
Expand Down Expand Up @@ -203,7 +203,7 @@
#end
##
#macro(exportTreeModal)
<div class="modal fade text-left" id="exportTreeModal" tabindex="-1" role="dialog"
<dialog class="modal fade text-left" id="exportTreeModal"
aria-labelledby="exportTreeModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
Expand Down Expand Up @@ -237,5 +237,5 @@
</div>
</div>
</div>
</div>
</dialog>
#end
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,6 @@
## Note: Both the admin actions and the more actions menus are now merged into one.
#if ($displayAdminMenu || $displayMoreActionsMenu)
#displayOptionsMenu()
#if ($canView)
#template("export_modal.vm")
#exportModal()
#end
#end
#end
#set ($discard = $topStaticExtensions.add( { 'content': "$!actionsMenu", 'order': 40000}))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -502,37 +502,160 @@
#set($discard = $xwiki.ssx.use('XWiki.Notifications.Code.NotificationsWatchUIX'))
#set($discard = $xwiki.jsx.use('XWiki.Notifications.Code.NotificationsWatchUIX'))
#set ($watchedStatus = $services.notification.watch.getLocationWatchedStatus($doc))
## If the watched status informs that the filter is about children it means it's a filter for the space,
## so we should look for ancestor of the space.
#set ($refForAncestorFirstFilter = $doc.getDocumentReference())
#if ($watchedStatus == 'WATCHED_WITH_CHILDREN_FOR_ALL_EVENTS_AND_FORMATS' || $watchedStatus == 'BLOCKED_WITH_CHILDREN_FOR_ALL_EVENTS_AND_FORMATS')
#set ($refForAncestorFirstFilter = $doc.getDocumentReference().parent)
#end
#set ($ancestorFirstFilter = $services.notification.watch.getFirstFilteredAncestor($refForAncestorFirstFilter))
#if ($ancestorFirstFilter)
#set ($ancestorRef = $ancestorFirstFilter.left)
#set ($ancestorWatchStatus = $ancestorFirstFilter.right)
#end
## FIXME: the different icons should use the icon service
#set ($iconWhenWatched = 'fa-bell')
#set ($iconWhenNotSet = 'fa-bell-o')
#set ($iconWhenBlocked = 'fa-bell-slash')
#set ($iconWhenCustom = 'fa-bullhorn')
#if ($watchedStatus == 'NOT_SET')
#set ($watchIcon = $iconWhenNotSet)
#set ($watchText = $services.localization.render('notifications.watch.button.status.notset', 'html/5.0', []))
#elseif ($watchedStatus.isWatched())
#set ($watchIcon = $iconWhenWatched)
#set ($watchText = $services.localization.render('notifications.watch.button.status.followed', 'html/5.0', []))
#elseif ($watchedStatus.isBlocked())
#set ($watchIcon = $iconWhenBlocked)
#set ($watchText = $services.localization.render('notifications.watch.button.status.blocked', 'html/5.0', []))
#else
#set ($watchIcon = $iconWhenCustom)
#set ($watchText = $services.localization.render('notifications.watch.button.status.custom', 'html/5.0', []))
#_notificationsWatchStatusIcon($watchedStatus)
#set ($buttonTitle = $services.localization.render('notifications.watch.button.title', [$watchText]))
{{html clean='false'}}
&lt;div class="btn-group" id="watchButton">
&lt;a href="#" role="button" title="$escapetool.xml($buttonTitle)" class="btn btn-default" data-toggle="modal" data-target="#watchModal">
&lt;span class="fa $watchIcon">&lt;/span> $watchText
&lt;/a>
&lt;/div>
{{/html}}
#end
## TODO: better way for that?
#set ($isTerminal = ($doc.getDocumentReference().name != 'WebHome'))
{{/velocity}}
</content>
</property>
<property>
<extensionPointId>org.xwiki.plaftorm.menu.content</extensionPointId>
</property>
<property>
<name>org.xwiki.platform.notification.watch</name>
</property>
<property>
<parameters>order=39000</parameters>
</property>
<property>
<scope>wiki</scope>
</property>
</object>
<object>
<name>XWiki.Notifications.Code.NotificationsWatchUIX</name>
<number>1</number>
<className>XWiki.UIExtensionClass</className>
<guid>ad3c491c-2532-48c7-8559-286a36a90cfb</guid>
<class>
<name>XWiki.UIExtensionClass</name>
<customClass/>
<customMapping/>
<defaultViewSheet/>
<defaultEditSheet/>
<defaultWeb/>
<nameField/>
<validationScript/>
<async_cached>
<defaultValue>0</defaultValue>
<disabled>0</disabled>
<displayFormType>select</displayFormType>
<displayType/>
<name>async_cached</name>
<number>3</number>
<prettyName>Cached</prettyName>
<unmodifiable>0</unmodifiable>
<classType>com.xpn.xwiki.objects.classes.BooleanClass</classType>
</async_cached>
<async_context>
<cache>0</cache>
<disabled>0</disabled>
<displayType>select</displayType>
<freeText>forbidden</freeText>
<largeStorage>0</largeStorage>
<multiSelect>1</multiSelect>
<name>async_context</name>
<number>4</number>
<prettyName>Context elements</prettyName>
<relationalStorage>0</relationalStorage>
<separator>, </separator>
<separators>|, </separators>
<size>5</size>
<unmodifiable>0</unmodifiable>
<values>action=Action|doc.reference=Document|doc.revision|icon.theme=Icon theme|locale=Language|rendering.defaultsyntax=Default syntax|rendering.restricted=Restricted|rendering.targetsyntax=Target syntax|request.base=Request base URL|request.cookies|request.headers|request.parameters=Request parameters|request.remoteAddr|request.session|request.url=Request URL|request.wiki=Request wiki|sheet|user=User|wiki=Wiki</values>
<classType>com.xpn.xwiki.objects.classes.StaticListClass</classType>
</async_context>
<async_enabled>
<defaultValue>0</defaultValue>
<disabled>0</disabled>
<displayFormType>select</displayFormType>
<displayType/>
<name>async_enabled</name>
<number>2</number>
<prettyName>Asynchronous rendering</prettyName>
<unmodifiable>0</unmodifiable>
<classType>com.xpn.xwiki.objects.classes.BooleanClass</classType>
</async_enabled>
<content>
<disabled>0</disabled>
<editor>Text</editor>
<name>content</name>
<number>1</number>
<prettyName>Executed Content</prettyName>
<restricted>0</restricted>
<rows>25</rows>
<size>120</size>
<unmodifiable>0</unmodifiable>
<classType>com.xpn.xwiki.objects.classes.TextAreaClass</classType>
</content>
<extensionPointId>
<disabled>0</disabled>
<name>extensionPointId</name>
<number>5</number>
<prettyName>Extension Point ID</prettyName>
<size>30</size>
<unmodifiable>0</unmodifiable>
<classType>com.xpn.xwiki.objects.classes.StringClass</classType>
</extensionPointId>
<name>
<disabled>0</disabled>
<name>name</name>
<number>6</number>
<prettyName>Extension ID</prettyName>
<size>30</size>
<unmodifiable>0</unmodifiable>
<classType>com.xpn.xwiki.objects.classes.StringClass</classType>
</name>
<parameters>
<contenttype>PureText</contenttype>
<disabled>0</disabled>
<editor>PureText</editor>
<name>parameters</name>
<number>7</number>
<prettyName>Extension Parameters</prettyName>
<restricted>0</restricted>
<rows>10</rows>
<size>40</size>
<unmodifiable>0</unmodifiable>
<classType>com.xpn.xwiki.objects.classes.TextAreaClass</classType>
</parameters>
<scope>
<cache>0</cache>
<disabled>0</disabled>
<displayType>select</displayType>
<freeText>forbidden</freeText>
<largeStorage>0</largeStorage>
<multiSelect>0</multiSelect>
<name>scope</name>
<number>8</number>
<prettyName>Extension Scope</prettyName>
<relationalStorage>0</relationalStorage>
<separator> </separator>
<separators>|, </separators>
<size>1</size>
<unmodifiable>0</unmodifiable>
<values>wiki=Current Wiki|user=Current User|global=Global</values>
<classType>com.xpn.xwiki.objects.classes.StaticListClass</classType>
</scope>
</class>
<property>
<async_cached>0</async_cached>
</property>
<property>
<async_context/>
</property>
<property>
<async_enabled>0</async_enabled>
</property>
<property>
<content>{{velocity}}
#if (!$isGuest)
#macro (_displayOption $attributeName $optionValue $translationSuffix $iconName $panelClass)
&lt;div class="panel panel-default $panelClass"&gt;
&lt;div class="panel-radio"&gt;
Expand All @@ -554,6 +677,24 @@
&lt;/div&gt;&lt;!-- end collapsed xhint --&gt;
&lt;/div&gt;&lt;!-- end panel --&gt;
#end

#set ($watchedStatus = $services.notification.watch.getLocationWatchedStatus($doc))
#_notificationsWatchStatusIcon($watchedStatus)
## If the watched status informs that the filter is about children it means it's a filter for the space,
## so we should look for ancestor of the space.
#set ($refForAncestorFirstFilter = $doc.getDocumentReference())
#if ($watchedStatus == 'WATCHED_WITH_CHILDREN_FOR_ALL_EVENTS_AND_FORMATS' || $watchedStatus == 'BLOCKED_WITH_CHILDREN_FOR_ALL_EVENTS_AND_FORMATS')
#set ($refForAncestorFirstFilter = $doc.getDocumentReference().parent)
#end
#set ($ancestorFirstFilter = $services.notification.watch.getFirstFilteredAncestor($refForAncestorFirstFilter))
#if ($ancestorFirstFilter)
#set ($ancestorRef = $ancestorFirstFilter.left)
#set ($ancestorWatchStatus = $ancestorFirstFilter.right)
#end

## TODO: better way for that?
#set ($isTerminal = ($doc.getDocumentReference().name != 'WebHome'))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes you can compare with the value returned for the default page for a DOCUMENT from the entity default resolver (don't recall the exact component name). We might even have something more elaborate for this as it's common.

I'm sure @tmortagne will know :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found an example at

#set ($sourcePageIsTerminal = $doc.documentReference.name != $services.model.getEntityReference('DOCUMENT', 'default').name)


#set ($watchPageOption = "#_displayOption('watch-page','watchPage','watchpage','page','')")
#set ($watchSpaceOption = "#_displayOption('watch-space','watchSpace','watchspace','chart-organisation','')")
#set ($unwatchPageAndWatchSpaceOption = "#_displayOption('unwatch-page-watch-space','unwatchPageWatchSpace','watchspace','chart-organisation','')")
Expand All @@ -567,15 +708,8 @@
#set ($unblockPageOption = "#_displayOption('unblock-page','unblockPage','unblockpage','page','')")
#set ($unblockSpaceOption = "#_displayOption('unblock-space','unblockSpace','unblockspace','chart-organisation','')")
#set ($unblockWikiOption = "#_displayOption('unblock-wiki','unblockWiki','unblockwiki','world','wiki-option')")

#set ($buttonTitle = $services.localization.render('notifications.watch.button.title', [$watchText]))
{{html clean='false'}}
&lt;div class="btn-group" id="watchButton"&gt;
&lt;a href="#" role="button" title="$escapetool.xml($buttonTitle)" class="btn btn-default" data-toggle="modal" data-target="#watchModal"&gt;
&lt;span class="fa $watchIcon"&gt;&lt;/span&gt; $watchText
&lt;/a&gt;
&lt;/div&gt;
&lt;div class="modal fade" tabindex="-1" role="dialog" id="watchModal"&gt;
&lt;dialog class="modal fade" id="watchModal"&gt;
&lt;div class="modal-dialog" role="document"&gt;
&lt;div class="modal-content"&gt;
&lt;div class="modal-header"&gt;
Expand Down Expand Up @@ -682,19 +816,19 @@
&lt;/div&gt;
&lt;/div&gt;&lt;!-- /.modal-content --&gt;
&lt;/div&gt;&lt;!-- /.modal-dialog --&gt;
&lt;/div&gt;&lt;!-- /.modal --&gt;
&lt;/dialog&gt;&lt;!-- /.modal --&gt;
{{/html}}
#end
{{/velocity}}</content>
</property>
<property>
<extensionPointId>org.xwiki.plaftorm.menu.content</extensionPointId>
<extensionPointId>org.xwiki.platform.template.body.end</extensionPointId>
</property>
<property>
<name>org.xwiki.platform.notification.watch</name>
<name>org.xwiki.platform.notification.watchModal</name>
</property>
<property>
<parameters>order=39000</parameters>
<parameters></parameters>
</property>
<property>
<scope>wiki</scope>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@
#if ($services.debug.isEnabled())
#template('debug.vm')
#end
<div class="modal-container">
Copy link
Contributor

@michitux michitux Jun 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class name seems a bit unexpected given the extension point's name. Also, what's the purpose of this <div>?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It made sense when doing it, but looking at it again I understand why it's not clear.

Technically, the modals are still at the end of the body, even though they are wrapped under a div.

Also, what's the purpose of this

?

It's a container so that whatever modal added by the extension point is a bit apart from the other elements in the body. This way it should be easier to select modals with CSS or JS and customize them.

IMO it can be useful for customizations, it makes it a bit easier to read the HTML and I believe that it won't be a liability on the long term (no real downside to adding this AFAIK).

Here's what the HTML view in the Firefox inspector looks like today. It's not easy to figure out that the first thing is the only thing that's shown, and all the others are just misc modals that might appear at some point.
image

Note that in XS, this new class doesn't collide with any existing node (quite close but still different from the quite old .xdialog-modal-container). We could also make it an id to avoid making collisions by mistake in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional pro: it's easier for technical users to understand where the UIXP aims at exactly, given we give the exact id/class on the UIXP documentation. This one is quite straightforward even without a container though :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd change the class name to reflect the fact that it's the wrapper for the end of DOM UIXP. The fact that we put modals here is then a technical decision that might change in the future (e.g. if we decide to only load them all async), but the UIX would still be included there. So I'd go for something like endofdom-container

#if (($displayAdminMenu || $displayMoreActionsMenu) && $canView)
#template("export_modal.vm")
#exportModal()
#end
## We don't care about order for this extension point, none of the extensions should have 'visible' content.
## The primary use of this extension point is to gather modal elements that did not get loaded asynchronously.
#foreach ($uix in $services.uix.getExtensions('org.xwiki.platform.template.body.end'))
$services.rendering.render($uix.execute(), 'html/5.0')
#end
</div>
</body>
</html>
#else ## Portlet Mode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3212,3 +3212,35 @@ Recursive title display detected!##
#macro (unwrapXPropertyDisplay $displayOutput)
$!stringtool.removeEnd($stringtool.removeStart($displayOutput, '{{html clean="false" wiki="false"}}'), '{{/html}}')##
#end

#**
* XWiki Standard use only.
* $watchStatus is a WatchedEntityReference.WatchedStatus returned by script services
* similar to $services.notification.watch.getLocationWatchedStatus
* Returns the FA4 class of the icon corresponding to the provided watchStatus in $watchIcon
* AND
* Returns the status label in $watchText
* @since 17.4.2
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the PR description you said that this won't be backported, why do you indicate 17.4.2 then?

* @since 17.5.0RC1
*#
#macro (_notificationsWatchStatusIcon $watchedStatus)
## FIXME: the different icons should use the icon service
## They should be added to the XWiki Icon set
#set ($iconWhenWatched = 'fa-bell')
#set ($iconWhenNotSet = 'fa-bell-o')
#set ($iconWhenBlocked = 'fa-bell-slash')
#set ($iconWhenCustom = 'fa-bullhorn')
#if ($watchedStatus == 'NOT_SET')
#set ($watchIcon = $iconWhenNotSet)
#set ($watchText = $services.localization.render('notifications.watch.button.status.notset', 'html/5.0', []))
#elseif ($watchedStatus.isWatched())
#set ($watchIcon = $iconWhenWatched)
#set ($watchText = $services.localization.render('notifications.watch.button.status.followed', 'html/5.0', []))
#elseif ($watchedStatus.isBlocked())
#set ($watchIcon = $iconWhenBlocked)
#set ($watchText = $services.localization.render('notifications.watch.button.status.blocked', 'html/5.0', []))
#else
#set ($watchIcon = $iconWhenCustom)
#set ($watchText = $services.localization.render('notifications.watch.button.status.custom', 'html/5.0', []))
#end
#end