Some improvments for the text editor

pull/10/head
Jens Pelzetter 2021-05-13 12:47:39 +02:00
parent 4a6478c78b
commit 3c486f7610
5 changed files with 433 additions and 207 deletions

View File

@ -203,9 +203,11 @@ public class MvcArticleTextBodyStep extends AbstractMvcAuthoringStep {
if (itemPermissionChecker.canEditItem(getArticle())) { if (itemPermissionChecker.canEditItem(getArticle())) {
final String value; final String value;
if (getArticle().getText().getAvailableLocales().isEmpty()) { if (getArticle().getText().getAvailableLocales().isEmpty()) {
value = "Lorem ipsum"; value = "";
} else { } else {
value = getArticle().getText().getValue(defaultLocale); value = globalizationHelper.getValueFromLocalizedString(
getArticle().getText()
);
} }
final Locale locale = new Locale(localeParam); final Locale locale = new Locale(localeParam);
getArticle().getText().addValue(locale, value); getArticle().getText().addValue(locale, value);
@ -253,8 +255,6 @@ public class MvcArticleTextBodyStep extends AbstractMvcAuthoringStep {
// ); // );
// } // }
// } // }
@GET @GET
@Path("/edit/{locale}") @Path("/edit/{locale}")
@Transactional(Transactional.TxType.REQUIRED) @Transactional(Transactional.TxType.REQUIRED)
@ -265,7 +265,7 @@ public class MvcArticleTextBodyStep extends AbstractMvcAuthoringStep {
final String documentPath, final String documentPath,
@PathParam("locale") final String localeParam @PathParam("locale") final String localeParam
) { ) {
try { try {
init(); init();
} catch (ContentSectionNotFoundException ex) { } catch (ContentSectionNotFoundException ex) {
return ex.showErrorMessage(); return ex.showErrorMessage();
@ -418,7 +418,8 @@ public class MvcArticleTextBodyStep extends AbstractMvcAuthoringStep {
private CmsEditorLocaleVariantRow buildVariantRow( private CmsEditorLocaleVariantRow buildVariantRow(
final Map.Entry<Locale, String> entry final Map.Entry<Locale, String> entry
) { ) {
final CmsEditorLocaleVariantRow variant = new CmsEditorLocaleVariantRow(); final CmsEditorLocaleVariantRow variant
= new CmsEditorLocaleVariantRow();
variant.setLocale(entry.getKey().toString()); variant.setLocale(entry.getKey().toString());
final Document document = Jsoup.parseBodyFragment(entry.getValue()); final Document document = Jsoup.parseBodyFragment(entry.getValue());
variant.setWordCount( variant.setWordCount(

View File

@ -63,10 +63,10 @@ public class MvcArticleTextBodyStepResources {
private ItemPermissionChecker itemPermissionChecker; private ItemPermissionChecker itemPermissionChecker;
@GET @GET
@Path("/variants/{locale}/wordcount") @Path("/variants/wordcount/{locale}")
@Produces(MediaType.TEXT_HTML) @Produces(MediaType.TEXT_HTML)
@Transactional(Transactional.TxType.REQUIRED) @Transactional(Transactional.TxType.REQUIRED)
public long getWordCount( public String getWordCount(
@PathParam(MvcAuthoringSteps.SECTION_IDENTIFIER_PATH_PARAM) @PathParam(MvcAuthoringSteps.SECTION_IDENTIFIER_PATH_PARAM)
final String sectionIdentifier, final String sectionIdentifier,
@PathParam(MvcAuthoringSteps.DOCUMENT_PATH_PATH_PARAM_NAME) @PathParam(MvcAuthoringSteps.DOCUMENT_PATH_PATH_PARAM_NAME)
@ -95,7 +95,8 @@ public class MvcArticleTextBodyStepResources {
.getText() .getText()
.getValue(new Locale(localeParam)); .getValue(new Locale(localeParam));
final Document jsoupDoc = Jsoup.parseBodyFragment(text); final Document jsoupDoc = Jsoup.parseBodyFragment(text);
return new StringTokenizer(jsoupDoc.body().text()).countTokens(); long result = new StringTokenizer(jsoupDoc.body().text()).countTokens();
return Long.toString(result);
} else { } else {
throw new ForbiddenException(); throw new ForbiddenException();
} }

View File

@ -183,6 +183,10 @@
<cc:attribute name="viewDialogCloseButtonLabel" <cc:attribute name="viewDialogCloseButtonLabel"
default="Close" default="Close"
type="String" /> type="String" />
<cc:attribute name="wordCountUrl"
required="true"
shortDescription="URL of the endpoint for retrieving the wordcount of a variant. The locale of the variant to retrieve is appended as last token."
type="String" />
</cc:interface> </cc:interface>
<cc:implementation> <cc:implementation>
<div class="cms-editor" <div class="cms-editor"
@ -358,6 +362,7 @@
data-locale="#{variant.locale}" data-locale="#{variant.locale}"
data-variant-url="#{cc.attrs.variantUrl}/#{variant.locale}" data-variant-url="#{cc.attrs.variantUrl}/#{variant.locale}"
data-save-url="#{cc.attrs.editMethod}/#{variant.locale}" data-save-url="#{cc.attrs.editMethod}/#{variant.locale}"
data-wordcount-url="#{cc.attrs.wordCountUrl}/#{variant.locale}"
type="button"> type="button">
<bootstrap:svgIcon icon="pen" /> <bootstrap:svgIcon icon="pen" />
<span class="sr-only"> <span class="sr-only">

View File

@ -25,6 +25,7 @@
unusedLocales="#{CmsArticleTextBodyStep.unusedLocales}" unusedLocales="#{CmsArticleTextBodyStep.unusedLocales}"
variantUrl="#{mvc.basePath}/#{ContentSectionModel.sectionName}/documents/#{CmsSelectedDocumentModel.itemPath}/@article-text-resources/variants" variantUrl="#{mvc.basePath}/#{ContentSectionModel.sectionName}/documents/#{CmsSelectedDocumentModel.itemPath}/@article-text-resources/variants"
variants="#{CmsArticleTextBodyStep.variants}" variants="#{CmsArticleTextBodyStep.variants}"
wordCountUrl="#{mvc.basePath}/#{ContentSectionModel.sectionName}/documents/#{CmsSelectedDocumentModel.itemPath}/@article-text-resources/variants/wordcount"
/> />
</ui:define> </ui:define>

View File

@ -3,211 +3,223 @@ import * as $ from "jquery";
import { Editor } from "@tiptap/core"; import { Editor } from "@tiptap/core";
import StarterKit from "@tiptap/starter-kit"; import StarterKit from "@tiptap/starter-kit";
function showMessage(messageId: string) {
const template = document.querySelector(messageId) as HTMLTemplateElement;
const msg = template.content.cloneNode(true);
document.querySelector(".cms-editor-messages").append(msg);
}
document.addEventListener("DOMContentLoaded", function (event) { document.addEventListener("DOMContentLoaded", function (event) {
document const viewButtons = document.querySelectorAll(
.querySelector( ".cms-editor .cms-editor-variants .cms-editor-view-button"
".cms-editor .cms-editor-variants .cms-editor-view-button" );
) for (let i = 0; i < viewButtons.length; i++) {
.addEventListener("click", function (event) { viewButtons[i].addEventListener("click", event =>
event.preventDefault(); showViewDialog(event)
);
}
const target = event.currentTarget as Element; const editButtons = document.querySelectorAll(
const variantUrl = target.getAttribute("data-variant-url"); ".cms-editor .cms-editor-variants .cms-editor-edit-button"
const viewDialogId = target.getAttribute("data-view-dialog"); );
for (let i = 0; i < editButtons.length; i++) {
editButtons[i].addEventListener("click", event =>
showEditDialog(event)
);
}
fetch(variantUrl, { // document
method: "GET", // .querySelector(
credentials: "include" // ".cms-editor .cms-editor-variants .cms-editor-view-button"
}) // )
.then(response => { // .addEventListener("click", function (event) {
if (response.ok) { // event.preventDefault();
response
.text()
.then(text => {
const viewDialog = document.querySelector(
`#${viewDialogId}`
);
const viewDialogBody = viewDialog.querySelector(
".modal-body"
);
viewDialogBody.textContent = text; // const target = event.currentTarget as Element;
// const variantUrl = target.getAttribute("data-variant-url");
// const viewDialogId = target.getAttribute("data-view-dialog");
$(`#${viewDialogId}`).modal("toggle"); // fetch(variantUrl, {
}) // method: "GET",
.catch(err => { // credentials: "include"
showMessage( // })
"#cms-editor-msg-variant-load-failed" // .then(response => {
); // if (response.ok) {
}); // response
} else { // .text()
showMessage("#cms-editor-msg-variant-load-failed"); // .then(text => {
} // const viewDialog = document.querySelector(
}) // `#${viewDialogId}`
.catch(err => { // );
showMessage("#cms-editor-msg-variant-load-failed"); // const viewDialogBody =
}); // viewDialog.querySelector(".modal-body");
});
document // viewDialogBody.textContent = text;
.querySelector(
".cms-editor .cms-editor-variants .cms-editor-edit-button"
)
.addEventListener("click", function (event) {
event.preventDefault();
const target = event.currentTarget as Element; // $(`#${viewDialogId}`).modal("toggle");
const locale = target.getAttribute("data-locale"); // })
const variantUrl = target.getAttribute("data-variant-url"); // .catch(err => {
const editDialogId = target.getAttribute("data-edit-dialog"); // showMessage(
const saveUrl = target.getAttribute("data-save-url"); // "#cms-editor-msg-variant-load-failed"
// );
// });
// } else {
// showMessage("#cms-editor-msg-variant-load-failed");
// }
// })
// .catch(err => {
// showMessage("#cms-editor-msg-variant-load-failed");
// });
// });
fetch(variantUrl, { // document
method: "GET", // .querySelector(
credentials: "include" // ".cms-editor .cms-editor-variants .cms-editor-edit-button"
}) // )
.then(response => { // .addEventListener("click", function (event) {
if (response.ok) { // event.preventDefault();
response
.text()
.then(text => {
const editDialog = document.querySelector(
`#${editDialogId}`
);
const tiptapDiv = editDialog.querySelector(
".modal-body .cms-tiptap-editor"
);
if (!tiptapDiv) {
console.warn("tiptapDiv is null");
}
const editor = new Editor({ // const target = event.currentTarget as Element;
element: tiptapDiv, // const locale = target.getAttribute("data-locale");
extensions: [StarterKit], // const variantUrl = target.getAttribute("data-variant-url");
content: text // const editDialogId = target.getAttribute("data-edit-dialog");
}); // const saveUrl = target.getAttribute("data-save-url");
const buttonsDiv = editDialog.querySelector( // fetch(variantUrl, {
".cms-tiptap-editor-buttons" // method: "GET",
); // credentials: "include"
if (!buttonsDiv) { // })
console.warn("buttonsDiv is null."); // .then(response => {
} // if (response.ok) {
const emphButton = buttonsDiv.querySelector( // response
".tiptap-emph" // .text()
); // .then(text => {
if (!emphButton) { // const editDialog = document.querySelector(
console.warn("emphButton not found."); // `#${editDialogId}`
} // );
emphButton.addEventListener("click", event => { // const tiptapDiv = editDialog.querySelector(
event.preventDefault(); // ".modal-body .cms-tiptap-editor"
editor.chain().focus().toggleItalic().run(); // );
}); // if (!tiptapDiv) {
// console.warn("tiptapDiv is null");
// }
const strongEmphButton = buttonsDiv.querySelector( // const editor = new Editor({
".tiptap-strong-emph" // element: tiptapDiv,
); // extensions: [StarterKit],
if (!strongEmphButton) { // content: text
console.warn("strongEmphButton not found."); // });
}
strongEmphButton.addEventListener(
"click",
event => {
event.preventDefault();
editor
.chain()
.focus()
.toggleBold()
.run();
}
);
const closeButton = editDialog.querySelector( // const buttonsDiv = editDialog.querySelector(
".modal-header .close" // ".cms-tiptap-editor-buttons"
); // );
const cancelButton = editDialog.querySelector( // if (!buttonsDiv) {
".modal-footer .cms-editor-cancel-button" // console.warn("buttonsDiv is null.");
); // }
const saveButton = editDialog.querySelector( // const emphButton =
".modal-footer .cms-editor-save-button" // buttonsDiv.querySelector(".tiptap-emph");
); // if (!emphButton) {
// console.warn("emphButton not found.");
// }
// emphButton.addEventListener("click", event => {
// event.preventDefault();
// editor.chain().focus().toggleItalic().run();
// });
closeButton.addEventListener("click", event => { // const strongEmphButton =
editor.chain().clearContent(); // buttonsDiv.querySelector(
editor.destroy(); // ".tiptap-strong-emph"
$(`#${editDialogId}`).modal("toggle"); // );
}); // if (!strongEmphButton) {
// console.warn("strongEmphButton not found.");
// }
// strongEmphButton.addEventListener(
// "click",
// event => {
// event.preventDefault();
// editor
// .chain()
// .focus()
// .toggleBold()
// .run();
// }
// );
cancelButton.addEventListener( // const closeButton = editDialog.querySelector(
"click", // ".modal-header .close"
event => { // );
editor.chain().clearContent(); // const cancelButton = editDialog.querySelector(
editor.destroy(); // ".modal-footer .cms-editor-cancel-button"
$(`#${editDialogId}`).modal("toggle"); // );
} // const saveButton = editDialog.querySelector(
); // ".modal-footer .cms-editor-save-button"
// );
saveButton.addEventListener("click", event => { // closeButton.addEventListener("click", event => {
const html = editor.getHTML(); // editor.chain().clearContent();
const params = new URLSearchParams(); // editor.destroy();
params.append("value", html); // $(`#${editDialogId}`).modal("toggle");
fetch(saveUrl, { // });
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: params
})
.then(saveResponse => {
if (saveResponse.ok) {
showMessage(
"#cms-editor-msg-save-successful"
);
window.location.reload();
} else {
showMessage(
"#cms-editor-msg-save-failed"
);
}
$(`#${editDialogId}`).modal(
"toggle"
);
})
.catch(err => {
showMessage(
"#cms-editor-msg-save-failed"
);
console.error(err);
$(`#${editDialogId}`).modal(
"toggle"
);
});
});
$(`#${editDialogId}`).modal("toggle"); // cancelButton.addEventListener(
}) // "click",
.catch(err => { // event => {
showMessage( // editor.chain().clearContent();
"#cms-editor-msg-variant-load-failed" // editor.destroy();
); // $(`#${editDialogId}`).modal("toggle");
console.error(err); // }
}); // );
} else {
showMessage("#cms-editor-msg-variant-load-failed"); // saveButton.addEventListener("click", event => {
} // const html = editor.getHTML();
}) // const params = new URLSearchParams();
.catch(err => { // params.append("value", html);
showMessage("#cms-editor-msg-variant-load-failed"); // fetch(saveUrl, {
console.error(err); // method: "POST",
}); // credentials: "include",
}); // headers: {
// "Content-Type":
// "application/x-www-form-urlencoded"
// },
// body: params
// })
// .then(saveResponse => {
// if (saveResponse.ok) {
// showMessage(
// "#cms-editor-msg-save-successful"
// );
// window.location.reload();
// } else {
// showMessage(
// "#cms-editor-msg-save-failed"
// );
// }
// $(`#${editDialogId}`).modal(
// "toggle"
// );
// })
// .catch(err => {
// showMessage(
// "#cms-editor-msg-save-failed"
// );
// console.error(err);
// $(`#${editDialogId}`).modal(
// "toggle"
// );
// });
// });
// $(`#${editDialogId}`).modal("toggle");
// })
// .catch(err => {
// showMessage(
// "#cms-editor-msg-variant-load-failed"
// );
// console.error(err);
// });
// } else {
// showMessage("#cms-editor-msg-variant-load-failed");
// }
// })
// .catch(err => {
// showMessage("#cms-editor-msg-variant-load-failed");
// console.error(err);
// });
// });
// console.log("Starting editor"); // console.log("Starting editor");
// new Editor({ // new Editor({
@ -218,3 +230,209 @@ document.addEventListener("DOMContentLoaded", function (event) {
// content: '<h1>Hello World</h1>' // content: '<h1>Hello World</h1>'
// }) // })
}); });
function closeEditor(event: Event, editor: Editor, editDialogId: string) {
event.preventDefault();
editor.chain().clearContent();
editor.destroy();
$(`#${editDialogId}`).modal("toggle");
}
async function fetchVariant(fromUrl: string) {
try {
const response = await fetch(fromUrl, {
method: "GET",
credentials: "include"
});
if (response.ok) {
return await response.text();
} else {
showLoadVariantFailedMessage(response.status, response.statusText);
}
} catch (err) {
showLoadVariantFailedErrMessage(err);
}
}
async function fetchWordCount(fromUrl: string) {
try {
const response = await fetch(fromUrl, {
method: "GET",
credentials: "include"
});
if (response.ok) {
return await response.text();
} else {
return "?";
}
} catch (err) {
return "?";
}
}
function initEditorButtons(editor: Editor, buttonsElem: Element) {
// const emphButton: HTMLButtonElement | null = buttonsElem.querySelector(
// ".tiptap-emph"
// );
// if(emphButton) {
// emphButton.addEventListener("click", event => {
// event.preventDefault();
// editor.chain().focus().toggleItalic().run();
// });
// }
buttonsElem
.querySelector(".tiptap-emph")
?.addEventListener("click", event => {
event.preventDefault();
editor.chain().focus().toggleItalic().run();
});
buttonsElem
.querySelector(".tiptap-strong-emph")
?.addEventListener("click", event => {
event.preventDefault();
editor.chain().focus().toggleBold().run();
});
}
async function showEditDialog(event: Event) {
event.preventDefault();
const target = event.currentTarget as Element;
const locale = target.getAttribute("data-locale");
const variantUrl = target.getAttribute("data-variant-url");
const editDialogId = target.getAttribute("data-edit-dialog");
const saveUrl = target.getAttribute("data-save-url");
const wordCountUrl = target.getAttribute("data-wordcount-url");
const variant = await fetchVariant(variantUrl);
const editDialog = document.querySelector(`#${editDialogId}`);
const tiptapDiv = editDialog.querySelector(
".modal-body .cms-tiptap-editor"
);
if (!tiptapDiv) {
console.warn("tiptapDiv is null");
return;
}
const editor = new Editor({
element: tiptapDiv,
extensions: [StarterKit],
content: variant
});
initEditorButtons(
editor,
document.querySelector(".cms-tiptap-editor-buttons")
);
editDialog
.querySelector(".modal-header .close")
.addEventListener("click", event =>
closeEditor(event, editor, editDialogId)
);
editDialog
.querySelector(".modal-footer .cms-editor-cancel-button")
.addEventListener("click", event =>
closeEditor(event, editor, editDialogId)
);
editDialog
.querySelector(".modal-footer .cms-editor-save-button")
.addEventListener("click", event =>
save(event, editor, editDialogId, saveUrl, locale, wordCountUrl)
);
$(`#${editDialogId}`).modal("toggle");
}
async function save(
event: Event,
editor: Editor,
editDialogId: string,
saveUrl: string,
locale: string,
wordCountUrl: string
) {
event.preventDefault();
const params = new URLSearchParams();
params.append("value", editor.getHTML());
try {
const response = await fetch(saveUrl, {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: params
});
if (response.ok) {
showSaveSuccessfulMessage();
} else {
showSaveFailedMessage(response.status, response.statusText);
}
} catch (err) {
showSaveFailedErrMessage(err);
}
$(`#${editDialogId}`).modal("toggle");
const wordCount = await fetchWordCount(wordCountUrl);
const wordCountSpan = document.querySelector(
`tr#variant-${locale} .wordcount`
);
wordCountSpan.textContent = wordCount;
}
function showLoadVariantFailedErrMessage(err) {
showMessage("#cms-editor-msg-variant-load-failed");
console.error(err);
}
function showLoadVariantFailedMessage(status: number, statusText: string) {
showMessage("#cms-editor-msg-variant-load-failed");
console.error(`HTTP Status: ${status}, statusText: ${statusText}`);
}
function showMessage(messageId: string) {
const template = document.querySelector(messageId) as HTMLTemplateElement;
const msg = template.content.cloneNode(true);
document.querySelector(".cms-editor-messages").append(msg);
}
function showSaveFailedErrMessage(err) {
showMessage("#cms-editor-msg-save-failed");
console.error(err);
}
function showSaveFailedMessage(status: number, statusText: string) {
showMessage("#cms-editor-msg-save-failed");
console.error(`HTTP Status: ${status}, statusText: ${statusText}`);
}
function showSaveSuccessfulMessage() {
showMessage("#cms-editor-msg-save-successful");
}
async function showViewDialog(event: Event) {
event.preventDefault();
const target = event.currentTarget as Element;
const variantUrl = target.getAttribute("data-variant-url");
const viewDialogId = target.getAttribute("data-view-dialog");
const variant = await fetchVariant(variantUrl);
const viewDialog = document.querySelector(`#${viewDialogId}`);
const viewDialogBody = viewDialog.querySelector(".modal-body");
viewDialogBody.innerHTML = variant;
$(`#${viewDialogId}`).modal("toggle");
}