Skip to content

Commit

Permalink
#3081: Make placeholder configurable for mandatory elements
Browse files Browse the repository at this point in the history
This makes sure that when specifying a default template for a
richTextEditor type of field, we can also define a placeholder for
the html elements having the class mandatory. This placeholder
is being used to prefill the element if this one becomes empty.
Both the class attribute and the placeholder attribute of an HTML
tag of type h1, h2, h3 or p are being persisted in the database. This
in order to be able to still know the mandatory elements and their
eventual placeholders when editing an object with rich text content.
  • Loading branch information
maradragan committed Jun 30, 2020
1 parent ca227c1 commit ef440b6
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 52 deletions.
2 changes: 1 addition & 1 deletion anet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ dictionary:
keyOutcomes: Key outcomes
reportText:
label: Key details
placeholder: <h1 class="mandatory" placeholder="Key details 1">Key details 1</h1><p class="mandatory" placeholder="Type here key details 1 extra information">Type here key details 1 extra information</p><h1 class="mandatory" placeholder="Key details 2">Key details 2</h1><p class="mandatory" placeholder="Type here key details 2 extra information">Type here key details 2 extra information</p><h1 class="mandatory" placeholder="Key details 3">Key details 3</h1><p class="mandatory" placeholder="Type here key details 3 extra information">Type here key details 3 extra information</p>
placeholder: <h1 class="mandatory" placeholder="Key details 1">Key details 1</h1><p class="mandatory" placeholder="Type here key details 1 extra information">Type here key details 1 extra information</p><h1 class="mandatory" placeholder="Key details 2">Key details 2</h1><p class="mandatory" placeholder="Type here key details 2 extra information">Type here key details 2 extra information</p><h1 class="mandatory" placeholder="Key details 3">Key details 3</h1><p class="mandatory" placeholder="Type here key details 3 extra information">Type here key details 3 extra information</p><h1>Extra key details</h1><p>Type here optional details</p>
customFields:
multipleButtons:
type: enumset
Expand Down
74 changes: 39 additions & 35 deletions client/src/components/RichTextEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from "draft-js-buttons"
import createSideToolbarPlugin from "draft-js-side-toolbar-plugin"
import { BLOCK_TYPE, DraftailEditor, ENTITY_TYPE, INLINE_STYLE } from "draftail"
import _isEmpty from "lodash/isEmpty"
import _isEqual from "lodash/isEqual"
import PropTypes from "prop-types"
import React, { Component } from "react"
Expand Down Expand Up @@ -90,6 +91,37 @@ const ENTITY_CONTROL = {
}
}

const BLOCK_TYPE_TAG_NAME = {
[BLOCK_TYPE.HEADER_ONE]: "h1",
[BLOCK_TYPE.HEADER_TWO]: "h2",
[BLOCK_TYPE.HEADER_THREE]: "h3",
[BLOCK_TYPE.UNSTYLED]: "p"
}

const htmlToBlockMiddleware = next => (nodeName, node, lastList) => {
if (nodeName === "hr" || nodeName === "img") {
// "atomic" blocks is how Draft.js structures block-level entities.
return "atomic"
}

const data = {}
if (node?.attributes?.class?.nodeValue === "mandatory") {
data.mandatory = true
}
const placeholder = node.attributes?.placeholder?.nodeValue
if (!_isEmpty(placeholder)) {
data.placeholder = placeholder
}
if (!_isEmpty(data)) {
const defaultBlock = next(nodeName, node, lastList)
const block =
typeof defaultBlock === "string" ? { type: defaultBlock } : defaultBlock
return { ...block, data }
}
return null
}
htmlToBlockMiddleware.__isMiddleware = true

const importerConfig = {
htmlToEntity: (nodeName, node, createEntity) => {
if (nodeName === "a") {
Expand All @@ -104,49 +136,21 @@ const importerConfig = {

return null
},
htmlToBlock: (nodeName, node) => {
if (
nodeName === "h1" &&
node.attributes?.class?.nodeValue === "mandatory"
) {
return {
type: BLOCK_TYPE.HEADER_ONE,
data: { mandatory: true }
}
}

if (nodeName === "p" && node.attributes?.class?.nodeValue === "mandatory") {
return {
type: BLOCK_TYPE.UNSTYLED,
data: { mandatory: true }
}
}

if (nodeName === "hr" || nodeName === "img") {
// "atomic" blocks is how Draft.js structures block-level entities.
return "atomic"
}

return null
},
htmlToBlock: htmlToBlockMiddleware,
htmlToStyle: (nodeName, node, currentStyle) => {
return currentStyle
}
}

const exporterConfig = {
blockToHTML: block => {
if (block.type === BLOCK_TYPE.HEADER_ONE && block.data.mandatory) {
return {
start: '<h1 class="mandatory">',
end: "</h1>"
}
}

if (block.type === BLOCK_TYPE.UNSTYLED && block.data.mandatory) {
const tagName = BLOCK_TYPE_TAG_NAME[block.type]
if (tagName && block.data.mandatory) {
return {
start: '<p class="mandatory">',
end: "</p>"
start: `<${tagName} class="mandatory" placeholder=${
block.data.placeholder || ""
}>`,
end: `</${tagName}>`
}
}

Expand Down
44 changes: 30 additions & 14 deletions client/src/components/editor/plugins/mandatoryBlockPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const replaceWithPlaceholder = (
nextState.getCurrentContent(),
nextState
.getSelection()
.set("isBackward", false)
.set("anchorOffset", startOffset)
.set("focusOffset", endOffset),
placeholder
Expand All @@ -35,35 +36,50 @@ const createMandatoryBlockPlugin = config => {
const currentContentBlock = contentState.getBlockForKey(anchorKey)
const currentContentBlockData = currentContentBlock.getData().toObject()
const currentContentBlockText = currentContentBlock.getText()
const mandatoryBlock = currentContentBlockData.mandatory
const cursorOffset = selectionState.anchorOffset
const { mandatory, placeholder } = currentContentBlockData
const { anchorOffset, focusOffset } = selectionState
const startOffset = selectionState.getStartOffset()
const endOffset = selectionState.getEndOffset()
const blockLength = currentContentBlockText.length
if (command === "backspace" && mandatoryBlock && cursorOffset === 0) {
if (
command === "backspace" &&
mandatory &&
anchorOffset === focusOffset &&
anchorOffset === 0
) {
// Prevent backspace when at the beginning of the block, to avoid
// merge with the previous block
// FIXME: handleKeyCommand is not being used in this case for the first
// content block, and when
// Note: for the first content block, a backspace in this context
// doesn't use handleKeyCommand, but that's not a problem, it becomes an
// unstyled mandatory element
return "handled"
}
if (
command === "delete" &&
mandatoryBlock &&
cursorOffset === blockLength
mandatory &&
anchorOffset === focusOffset &&
anchorOffset === blockLength
) {
// Prevent delete when at the end of the block, to avoid
// merge with the next block
return "handled"
}
if (
mandatoryBlock &&
blockLength === 1 &&
((command === "delete" && cursorOffset === 0) ||
(command === "backspace" && cursorOffset === 1))
placeholder &&
mandatory &&
((blockLength === 1 && command === "delete" && anchorOffset === 0) ||
(blockLength === 1 &&
command === "backspace" &&
anchorOffset === 1) ||
(blockLength === endOffset - startOffset &&
["backspace", "delete"].includes(command)))
) {
// When a placeholder is given, instead of deleting last character,
// replace it with the placeholder
// When a placeholder is given, instead of deleting last left character or
// instead of deleting the whole text content, replace it with placeholder
const nextState = editorState
setEditorState(replaceWithPlaceholder(nextState, "placeholder", 0, 1))
setEditorState(
replaceWithPlaceholder(nextState, placeholder, 0, blockLength)
)
return "handled"
}
}
Expand Down
5 changes: 3 additions & 2 deletions src/main/java/mil/dds/anet/utils/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,9 @@ public static Map<String, Task> buildParentTaskMapping(List<Task> tasks,
// Defeat link spammers.
.requireRelNofollowOnLinks()
// The align attribute on <p> elements can have any value below.
.allowAttributes("align").matching(true, "center", "left", "right", "justify", "char").onElements("p")
.allowAttributes("class").onElements("h1", "p")
.allowAttributes("align").matching(true, "center", "left", "right", "justify", "char")
.onElements("p").allowAttributes("class", "placeholder")
.onElements("h1", "h2", "h3", "h4", "h5", "h6", "p")
.allowAttributes("border", "cellpadding", "cellspacing").onElements("table")
.allowAttributes("colspan", "rowspan").onElements("td", "th").allowStyling()
// These elements are allowed.
Expand Down

0 comments on commit ef440b6

Please sign in to comment.