IMprovements for user management UI

Former-commit-id: 444f59f760
pull/7/head
Jens Pelzetter 2020-09-28 21:11:37 +02:00
parent db19646e49
commit be2e61de81
9 changed files with 519 additions and 187 deletions

View File

@ -19,7 +19,9 @@
package org.libreccm.ui.admin.usersgroupsroles;
import org.libreccm.core.EmailAddress;
import org.libreccm.security.Group;
import org.libreccm.security.GroupMembership;
import org.libreccm.security.GroupRepository;
import org.libreccm.security.RoleMembership;
import org.libreccm.security.User;
import org.libreccm.ui.Message;
@ -31,6 +33,7 @@ import java.util.Objects;
import java.util.stream.Collectors;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.inject.Named;
import javax.transaction.Transactional;
@ -42,6 +45,9 @@ import javax.transaction.Transactional;
@Named("UserDetailsModel")
public class UserDetailsModel {
@Inject
private GroupRepository groupRepository;
private long userId;
private String uuid;
@ -149,6 +155,14 @@ public class UserDetailsModel {
return Collections.unmodifiableList(groupMemberships);
}
public List<UserGroupsFormEntry> getUserGroupsFormEntries() {
return groupRepository
.findAll()
.stream()
.map(this::buildUserGroupsFormEntry)
.collect(Collectors.toList());
}
public List<PartyRoleMembership> getRoles() {
return Collections.unmodifiableList(roles);
}
@ -156,4 +170,19 @@ public class UserDetailsModel {
public boolean isNewUser() {
return userId == 0;
}
private UserGroupsFormEntry buildUserGroupsFormEntry(final Group group) {
final UserGroupsFormEntry entry = new UserGroupsFormEntry();
entry.setGroupId(group.getPartyId());
entry.setGroupName(group.getName());
entry.setGroupUuid(group.getUuid());
entry.setMember(
groupMemberships
.stream()
.anyMatch(
membership -> membership.getGroupUuid().equals(group.getUuid())
)
);
return entry;
}
}

View File

@ -36,6 +36,7 @@ import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.mvc.Controller;
import javax.mvc.Models;
import javax.mvc.MvcContext;
import javax.mvc.binding.BindingResult;
import javax.mvc.binding.MvcBinding;
import javax.transaction.Transactional;
@ -67,6 +68,9 @@ public class UserFormController {
@Inject
private Models models;
@Inject
private MvcContext mvc;
@Inject
private UserManager userManager;
@ -201,7 +205,28 @@ public class UserFormController {
));
return "org/libreccm/ui/admin/users-groups-roles/user-form.xhtml";
}
}
@POST
@Path("{userIdentifier}/groups")
@AuthorizationRequired
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
@Transactional(Transactional.TxType.REQUIRED)
public String updateGroupMemberships(
@PathParam("userIdentifier") final String userIdentifierParam,
@FormParam("userGroups") final String[] userGroups
) {
// ToDo
return String.format(
"redirect:%s",
mvc.uri(
String.format(
"UsersController#getUserDetails",
"{userIdentifier: %s}",
userIdentifierParam
)
)
);
}
}

View File

@ -0,0 +1,68 @@
/*
* Copyright (C) 2020 LibreCCM Foundation.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package org.libreccm.ui.admin.usersgroupsroles;
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
public class UserGroupsFormEntry {
private long groupId;
private String groupUuid;
private String groupName;
private boolean member;
public long getGroupId() {
return groupId;
}
public void setGroupId(final long groupId) {
this.groupId = groupId;
}
public String getGroupUuid() {
return groupUuid;
}
public void setGroupUuid(final String groupUuid) {
this.groupUuid = groupUuid;
}
public String getGroupName() {
return groupName;
}
public void setGroupName(final String groupName) {
this.groupName = groupName;
}
public boolean isMember() {
return member;
}
public void setMember(final boolean member) {
this.member = member;
}
}

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!DOCTYPE html [<!ENTITY times '&#215;'>]>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core"
xmlns:h="http://xmlns.jcp.org/jsf/html"
@ -118,7 +118,7 @@
</dd>
</div>
</dl>
<a class="btn btn-info" href="#">
<a class="btn btn-primary" href="#">
<svg class="bi"
width="1em"
height="1em"
@ -130,211 +130,264 @@
</span>
</a>
<c:if test="#{UserDetailsModel.emailAddresses.size() > 0}">
<h2>
<div class="d-flex">
<h2 class="mr-2">
#{AdminMessages['usergroupsroles.users.user_details.additional_email_addresses.heading']}
</h2>
<a class="btn btn-secondary" href="#">
<svg class="bi"
width="1em"
height="1em"
fill="currentColor">
<use xlink:href="#{request.contextPath}/assets/bootstrap/bootstrap-icons.svg#plus-circle" />
</svg>
<span>#{AdminMessages['usergroupsroles.users.user_details.additional_email_addresses.add']}</span>
</a>
<table class="table table-hover">
<thead class="thead-light">
<tr>
<th>
#{AdminMessages['usergroupsroles.users.user_details.additional_email_addresses.cols.address']}
</th>
<th class="text-center">
#{AdminMessages['usergroupsroles.users.user_details.additional_email_addresses.cols.boucing']}
</th>
<th class="text-center">
#{AdminMessages['usergroupsroles.users.user_details.additional_email_addresses.cols.verified']}
</th>
<th class="text-center" colspan="2">
#{AdminMessages['usergroupsroles.users.user_details.additional_email_addresses.cols.actions']}
</th>
</tr>
<c:forEach items="#{UserDetailsModel.emailAddresses}"
var="address"
varStatus="status">
<div>
<a class="btn btn-primary" href="#">
<svg class="bi"
width="1em"
height="1em"
fill="currentColor">
<use xlink:href="#{request.contextPath}/assets/bootstrap/bootstrap-icons.svg#plus-circle" />
</svg>
<span>#{AdminMessages['usergroupsroles.users.user_details.additional_email_addresses.add']}</span>
</a>
</div>
</div>
<c:choose>
<c:when test="#{UserDetailsModel.emailAddresses.size() > 0}">
<table class="table table-hover">
<thead class="thead-light">
<tr>
<td>
#{address.address}
</td>
<td>
<c:choose>
<c:when test="#{address.bouncing}">
#{AdminMessages['usergroupsroles.users.user_details.email_address.bouncing.yes']}
</c:when>
<c:otherwise>
#{AdminMessages['usergroupsroles.users.user_details.email_address.bouncing.no']}
</c:otherwise>
</c:choose>
</td>
<td>
<c:choose>
<c:when test="#{address.verified}">
#{AdminMessages['usergroupsroles.users.user_details.email_address.verified.yes']}
</c:when>
<c:otherwise>
#{AdminMessages['usergroupsroles.users.user_details.email_address.verified.no']}
</c:otherwise>
</c:choose>
</td>
<td>
<a class="btn btn-info" href="#">
<svg class="bi"
width="1em"
height="1em"
fill="currentColor">
<use xlink:href="#{request.contextPath}/assets/bootstrap/bootstrap-icons.svg#pen" />
</svg>
<span>
#{AdminMessages['usergroupsroles.users.user_details.email_addresses.edit']}
</span>
</a>
</td>
<td>
<button class="btn btn-danger"
data-toggle="modal"
data-target="#confirm-remove-#{status.index}">
<svg class="bi"
width="1em"
height="1em"
fill="currentColor">
<use xlink:href="#{request.contextPath}/assets/bootstrap/bootstrap-icons.svg#x-circle" />
</svg>
<span>
#{AdminMessages['usergroupsroles.users.user_details.email_addresses.remove']}
</span>
</button>
<div class="modal"
id="confirm-remove-#{status.index}"
tabindex="-1">
<div class="modal-dialog">
<form action="#{mvc.uri('UsersController#removeEmailAddress', { 'userIdentifier': user.name, 'emailId': status.index })}"
class="modal-content"
method="post">
<div class="modal-header">
<h3 class="modal-title">
#{AdminMessages['usergroupsroles.users.user_details.email_addresses.remove.confirm.title']}
</h3>
<button aria-label="#{AdminMessages['usergroupsroles.users.user_details.email_addresses.remove.confirm.cancel']}"
class="close"
data-dismiss="modal"
type="button">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
#{AdminMessages.getMessage('usergroupsroles.users.user_details.email_addresses.remove.confirm.message', [address.address])}
<input name="confirmed"
type="hidden"
value="true" />
</div>
<div class="modal-footer">
<button class="btn btn-secondary"
data-dismiss="modal"
type="button">
#{'usergroupsroles.users.user_details.email_addresses.remove.confirm.cancel'}
</button>
<button class="btn btn-danger"
data-dismiss="modal"
type="submit">
#{'usergroupsroles.users.user_details.email_addresses.remove.confirm.yes'}
</button>
</div>
</form>
</div>
</div>
</td>
<th>
#{AdminMessages['usergroupsroles.users.user_details.additional_email_addresses.cols.address']}
</th>
<th class="text-center">
#{AdminMessages['usergroupsroles.users.user_details.additional_email_addresses.cols.boucing']}
</th>
<th class="text-center">
#{AdminMessages['usergroupsroles.users.user_details.additional_email_addresses.cols.verified']}
</th>
<th class="text-center" colspan="2">
#{AdminMessages['usergroupsroles.users.user_details.additional_email_addresses.cols.actions']}
</th>
</tr>
</c:forEach>
</thead>
</table>
</c:if>
<c:forEach items="#{UserDetailsModel.emailAddresses}"
var="address"
varStatus="status">
<tr>
<td>
#{address.address}
</td>
<td>
<c:choose>
<c:when test="#{address.bouncing}">
#{AdminMessages['usergroupsroles.users.user_details.email_address.bouncing.yes']}
</c:when>
<c:otherwise>
#{AdminMessages['usergroupsroles.users.user_details.email_address.bouncing.no']}
</c:otherwise>
</c:choose>
</td>
<td>
<c:choose>
<c:when test="#{address.verified}">
#{AdminMessages['usergroupsroles.users.user_details.email_address.verified.yes']}
</c:when>
<c:otherwise>
#{AdminMessages['usergroupsroles.users.user_details.email_address.verified.no']}
</c:otherwise>
</c:choose>
</td>
<td>
<a class="btn btn-info" href="#">
<svg class="bi"
width="1em"
height="1em"
fill="currentColor">
<use xlink:href="#{request.contextPath}/assets/bootstrap/bootstrap-icons.svg#pen" />
</svg>
<span>
#{AdminMessages['usergroupsroles.users.user_details.email_addresses.edit']}
</span>
</a>
</td>
<td>
<button class="btn btn-danger"
data-toggle="modal"
data-target="#confirm-remove-#{status.index}">
<svg class="bi"
width="1em"
height="1em"
fill="currentColor">
<use xlink:href="#{request.contextPath}/assets/bootstrap/bootstrap-icons.svg#x-circle" />
</svg>
<span>
#{AdminMessages['usergroupsroles.users.user_details.email_addresses.remove']}
</span>
</button>
<div class="modal"
id="confirm-remove-#{status.index}"
tabindex="-1">
<div class="modal-dialog">
<form action="#{mvc.uri('UsersController#removeEmailAddress', { 'userIdentifier': user.name, 'emailId': status.index })}"
class="modal-content"
method="post">
<div class="modal-header">
<h3 class="modal-title">
#{AdminMessages['usergroupsroles.users.user_details.email_addresses.remove.confirm.title']}
</h3>
<button aria-label="#{AdminMessages['usergroupsroles.users.user_details.email_addresses.remove.confirm.cancel']}"
class="close"
data-dismiss="modal"
type="button">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
#{AdminMessages.getMessage('usergroupsroles.users.user_details.email_addresses.remove.confirm.message', [address.address])}
<input name="confirmed"
type="hidden"
value="true" />
</div>
<div class="modal-footer">
<button class="btn btn-secondary"
data-dismiss="modal"
type="button">
#{'usergroupsroles.users.user_details.email_addresses.remove.confirm.cancel'}
</button>
<button class="btn btn-danger"
data-dismiss="modal"
type="submit">
#{'usergroupsroles.users.user_details.email_addresses.remove.confirm.yes'}
</button>
</div>
</form>
</div>
</div>
</td>
</tr>
</c:forEach>
</thead>
</table>
</c:when>
<c:otherwise>
<div class="alert alert-info" role="alert">
#{AdminMessages['usergroupsroles.users.user_details.email_addresses.none']}
</div>
</c:otherwise>
</c:choose>
<c:if test="#{UserDetailsModel.groupMemberships.size() > 0}">
<h2>
<div class="d-flex mb-1">
<h2 class="mr-2">
#{AdminMessages['usergroupsroles.users.user_details.groups.heading']}
</h2>
<a class="btn btn-secondary" href="#">
<button class="btn btn-primary"
data-toggle="modal"
data-target="#user-groups-dialog"
type="button">
<svg class="bi"
width="1em"
height="1em"
fill="currentColor">
<use xlink:href="#{request.contextPath}/assets/bootstrap/bootstrap-icons.svg#plus-circle" />
<use xlink:href="#{request.contextPath}/assets/bootstrap/bootstrap-icons.svg#pen" />
</svg>
<span>#{AdminMessages['usergroupsroles.users.user_details.groups.add']}</span>
</a>
<ul class="list-group mt-1">
<c:forEach items="#{UserDetailsModel.groupMemberships}"
var="group">
<li class="list-group-item d-flex justify-content-between align-items-center">
<a href="#">
#{group.groupName}
</a>
<a class="btn btn-danger" href="#">
<svg class="bi"
width="1em"
height="1em"
fill="currentColor">
<use xlink:href="#{request.contextPath}/assets/bootstrap/bootstrap-icons.svg#x-circle" />
</svg>
<span>
#{AdminMessages['usergroupsroles.users.user_details.groups.remove']}
</span>
</a>
</li>
</c:forEach>
</ul>
</c:if>
<span>#{AdminMessages['usergroupsroles.users.user_details.groups.edit']}</span>
</button>
<div aria-labelledby="user-groups-dialog-title"
aria-hidden="true"
class="modal fade"
data-backdrop="static"
id="user-groups-dialog"
tabindex="-1">
<div class="modal-dialog">
<form action=""
class="modal-content"
method="post">
<div class="modal-header">
<h3 class="modal-title"
id="user-groups-dialog-title">
#{AdminMessages['usergroupsroles.users.user_details.groups.dialog.title']}
</h3>
<button aria-label="#{AdminMessages['usergroupsroles.users.user_details.groups.dialog.close']}"
class="close"
data-dismiss="modal"
type="button">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<c:forEach items="#{UserDetailsModel.userGroupsFormEntries}"
var="entry">
<input checked="#{entry.member ? 'checked' : 'false'}"
id="group-#{entry.groupName}"
name="userGroups[]"
value="#{entry.groupName}"
type="checkbox" />
</c:forEach>
</div>
<div class="modal-footer">
<button class="btn btn-secondary"
data-dismiss="modal"
type="button" >
#{AdminMessages['usergroupsroles.users.user_details.groups.dialog.close']}
</button>
<button type="submit" class="btn btn-primary">
#{AdminMessages['usergroupsroles.users.user_details.groups.dialog.save']}
</button>
</div>
</form>
</div>
</div>
</div>
<c:choose>
<c:when test="#{UserDetailsModel.groupMemberships.size() > 0}">
<ul class="list-group mt-1">
<c:forEach items="#{UserDetailsModel.groupMemberships}">
<li class="list-group-item">
<a href="#">
#{group.groupName}
</a>
</li>
</c:forEach>
</ul>
</c:when>
<c:otherwise>
<div class="alert alert-info" role="alert">
#{AdminMessages['usergroupsroles.users.user_details.groups.none']}
</div>
</c:otherwise>
</c:choose>
<c:if test="#{UserDetailsModel.roles.size() > 0}">
<h2>
<div class="d-flex mb-1">
<h2 class="mr-2">
#{AdminMessages['usergroupsroles.users.user_details.roles.heading']}
</h2>
<a class="btn btn-secondary" href="#">
<button class="btn btn-primary" type="button">
<svg class="bi"
width="1em"
height="1em"
fill="currentColor">
<use xlink:href="#{request.contextPath}/assets/bootstrap/bootstrap-icons.svg#plus-circle" />
<use xlink:href="#{request.contextPath}/assets/bootstrap/bootstrap-icons.svg#pen" />
</svg>
<span>#{AdminMessages['usergroupsroles.users.user_details.roles.add']}</span>
</a>
<ul class="list-group mt-1">
<c:forEach items="#{UserDetailsModel.roles}"
var="role">
<li class="list-group-item d-flex justify-content-between align-items-center">
<a href="#">
#{role.roleName}
</a>
<a class="btn btn-danger" href="#">
<svg class="bi"
width="1em"
height="1em"
fill="currentColor">
<use xlink:href="#{request.contextPath}/assets/bootstrap/bootstrap-icons.svg#x-circle" />
</svg>
<span>
#{AdminMessages['usergroupsroles.users.user_details.roles.remove']}
</span>
</a>
</li>
</c:forEach>
</ul>
</c:if>
<span>#{AdminMessages['usergroupsroles.users.user_details.roles.edit']}</span>
</button>
</div>
<c:choose>
<c:when test="#{UserDetailsModel.roles.size() > 0}">
<ul class="list-group mt-1 mb-4">
<c:forEach items="#{UserDetailsModel.roles}" var="role">
<li class="list-group-item">
<a href="#">
#{role.roleName}
</a>
</li>
</c:forEach>
</ul>
</c:when>
<c:otherwise>
<div class="alert alert-info" role="alert">
#{AdminMessages['usergroupsroles.users.user_details.roles.none']}
</div>
</c:otherwise>
</c:choose>
</ui:define>
</ui:composition>
</html>

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!DOCTYPE html [<!ENTITY times '&#215;'>]>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core"
xmlns:h="http://xmlns.jcp.org/jsf/html"

View File

@ -115,3 +115,12 @@ usergroupsroles.users.user_details.email_addresses.remove.confirm.title=Confirm
usergroupsroles.users.user_details.email_addresses.remove.confirm.cancel=Cancel
usergroupsroles.users.user_details.email_addresses.remove.confirm.message=Are you sure to remove email address {0}?
usergroupsroles.users.user_details.email_addresses.remove.confirm.yes=Remove Email Address
usergroupsroles.users.user_details.groups.none=This user is not a member of any group
usergroupsroles.users.user_details.groups.edit=Edit
usergroupsroles.users.user_details.roles.edit=Edit
usergroupsroles.users.user_details.roles.none=No roles assigned to this user
usergroupsroles.users.user_details.email_addresses.none=This user has no additional email addresses
usergroupsroles.users.user_details.additional_email_addresses.add=Add email address
usergroupsroles.users.user_details.groups.dialog.title=Edit groupmemberships
usergroupsroles.users.user_details.groups.dialog.close=Cancel
usergroupsroles.users.user_details.groups.dialog.save=Save

View File

@ -115,3 +115,12 @@ usergroupsroles.users.user_details.email_addresses.remove.confirm.title=Entferne
usergroupsroles.users.user_details.email_addresses.remove.confirm.cancel=Abbrechen
usergroupsroles.users.user_details.email_addresses.remove.confirm.message=Sind Sie sicher, dass Sie die E-Mail-Addresse {0} entfernen wollen?
usergroupsroles.users.user_details.email_addresses.remove.confirm.yes=E-Mail-Addresse entfernen
usergroupsroles.users.user_details.groups.none=Diese(r) Benutzer*in ist nicht Mitglied einer Gruppe
usergroupsroles.users.user_details.groups.edit=Bearbeiten
usergroupsroles.users.user_details.roles.edit=Bearbeiten
usergroupsroles.users.user_details.roles.none=Dieser(m) Benutzer*in sind keine Rollen zugeordnet
usergroupsroles.users.user_details.email_addresses.none=Diese(r) Benutzer*in hat keine weiteren E-Mail-Addressen
usergroupsroles.users.user_details.additional_email_addresses.add=E-Mail-Addresse hinzuf\u00fcgen
usergroupsroles.users.user_details.groups.dialog.title=Gruppenmitgliedschaften bearbeiten
usergroupsroles.users.user_details.groups.dialog.close=Abbrechen
usergroupsroles.users.user_details.groups.dialog.save=Anwenden

View File

@ -1 +1,5 @@
import "bootstrap";
import { initFilterables } from "./filterable-list";
document.addEventListener("DOMContentReady", event => initFilterables());

View File

@ -0,0 +1,135 @@
export function initFilterables(): void {
document
.querySelectorAll("*[data-filter]")
.forEach(filterable => initFilterable(filterable));
}
function buildList(
filterable: Element,
options: Record<string, string>[],
template: HTMLTemplateElement,
filterInput: HTMLInputElement,
filterBy: string[]
) {
console.log("(Re-)Building list...");
console.dir(filterOptions);
filterable.innerHTML = "";
const filteredOptions = filterInput.value
? filterOptions(options, filterInput.value, filterBy)
: options;
for (const option of filteredOptions) {
const item = template.content.cloneNode(true);
replacePlaceholders(item, option);
filterable.appendChild(item);
}
}
function filterOption(
option: Record<string, string>,
filter: string,
filterBy: string[]
) {
let result: boolean = false;
for (const filterByProp of filterBy) {
result = result || option[filterByProp].indexOf(filter) !== -1;
}
return result;
}
function filterOptions(
options: Record<string, string>[],
filterValue: string,
filterBy: string[]
) {
return options.filter(option =>
filterOption(option, filterValue, filterBy)
);
}
function getFilterBy(filterable): string[] {
const filterByValue: string = filterable.getAttribute("data-filter-by");
if (filterByValue) {
return filterByValue.split(",");
} else {
return [];
}
}
function getFilterInput(filterable): HTMLInputElement {
const filterInputId: string = filterable.getAttribute("data-filter");
return document.querySelector(`input${filterInputId}`);
}
function getOptions(filterable: Element): Record<string, string>[] {
const attrValue: string = filterable.getAttribute("data-options");
if (attrValue.startsWith("#")) {
const dataScript: Element = document.querySelector(
`script${attrValue}`
);
return JSON.parse(dataScript.textContent);
} else {
return JSON.parse(attrValue);
}
}
function getTemplate(filterable: Element): HTMLTemplateElement {
const templateId: string = filterable.getAttribute("data-template");
return document.querySelector(templateId);
}
function initFilterable(filterable: Element): void {
const options: Record<string, string>[] = getOptions(filterable);
const template = getTemplate(filterable);
const filterInput = getFilterInput(filterable);
const filterBy = getFilterBy(filterable);
filterInput.addEventListener("keyup", event =>
buildList(filterable, options, template, filterInput, filterBy)
);
buildList(filterable, options, template, filterInput, filterBy);
}
function replacePlaceholders(node: Node, data: Record<string, string>) {
switch (node.nodeType) {
case Node.ELEMENT_NODE: {
const childNodes = node.childNodes;
for (let i = 0; i < childNodes.length; i++) {
replacePlaceholders(childNodes[i], data);
}
break;
}
case Node.TEXT_NODE: {
for (const key in data) {
console.log(`replacing ${key} with ${data[key]}`);
node.textContent = node.textContent.replace(
`{{${key}}}`,
data[key]
);
}
break;
}
case Node.CDATA_SECTION_NODE:
return;
case Node.DOCUMENT_NODE: {
const childNodes = node.childNodes;
for (let i = 0; i < childNodes.length; i++) {
replacePlaceholders(childNodes[i], data);
}
break;
}
case Node.DOCUMENT_FRAGMENT_NODE: {
const childNodes = node.childNodes;
for (let i = 0; i < childNodes.length; i++) {
replacePlaceholders(childNodes[i], data);
}
break;
}
default:
return;
}
}