diff --git a/ccm-cms/src/main/java/org/librecms/assets/VideoAsset.java b/ccm-cms/src/main/java/org/librecms/assets/VideoAsset.java index cd7a3ffab..a49526dab 100644 --- a/ccm-cms/src/main/java/org/librecms/assets/VideoAsset.java +++ b/ccm-cms/src/main/java/org/librecms/assets/VideoAsset.java @@ -19,6 +19,7 @@ package org.librecms.assets; import com.arsdigita.cms.ui.assets.forms.AudioForm; + import org.hibernate.envers.Audited; import java.io.Serializable; diff --git a/ccm-cms/src/main/java/org/librecms/contentsection/rs/Assets.java b/ccm-cms/src/main/java/org/librecms/contentsection/rs/Assets.java index e8d0e6fc4..a3b8d20de 100644 --- a/ccm-cms/src/main/java/org/librecms/contentsection/rs/Assets.java +++ b/ccm-cms/src/main/java/org/librecms/contentsection/rs/Assets.java @@ -53,9 +53,9 @@ import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; /** - * Provides a Web Service (build using JAX-RS). Used for example by the + * Provides a Web Service (build using JAX-RS). Used for example by the * {@link AssetSearchWidget}. - * + * * @author Jens Pelzetter */ @RequestScoped @@ -67,7 +67,7 @@ public class Assets { @Inject private FolderRepository folderRepo; - + @Inject private FolderManager folderManager; @@ -76,7 +76,7 @@ public class Assets { @Inject private AssetManager assetManager; - + @Inject private AssetTypesManager assetTypesManager; @@ -116,16 +116,18 @@ public class Assets { result.put("type", Folder.class.getName()); result.put("place", ""); - + return result; } private Map createAssetMapEntry(final Asset asset) { final Map result = new HashMap<>(); - result.put("assetId", + result.put("assetId", Long.toString(asset.getObjectId())); - + + result.put("uuid", asset.getUuid()); + result.put("title", asset.getTitle().getValue(globalizationHelper .getNegotiatedLocale())); @@ -142,12 +144,12 @@ public class Assets { final Optional assetFolder = assetManager.getAssetFolder(asset); if (assetFolder.isPresent()) { - result.put("place", - folderManager.getFolderPath(assetFolder.get())); + result.put("place", + folderManager.getFolderPath(assetFolder.get())); } else { result.put("place", ""); } - + return result; } diff --git a/ccm-editor/src/main/typescript/ccm-editor/ccm-cms-editor.ts b/ccm-editor/src/main/typescript/ccm-editor/ccm-cms-editor.ts index 9080e98dc..75e2c8ee6 100644 --- a/ccm-editor/src/main/typescript/ccm-editor/ccm-cms-editor.ts +++ b/ccm-editor/src/main/typescript/ccm-editor/ccm-cms-editor.ts @@ -62,11 +62,11 @@ export class InsertInternalLinkCommand extends CCMEditorCommand { .setAttribute("id", "ccm-editor-contentsection-select"); const filterInputLabel: Element = filterForm .appendChild(document.createElement("label")); - filterInputLabel.setAttribute("id", "ccm-editor-itemfilter"); + filterInputLabel.setAttribute("id", "ccm-editor-assetfilter"); filterInputLabel.textContent = "Filter items"; const filterInput: HTMLInputElement = filterForm .appendChild(document.createElement("input")); - filterInput.setAttribute("id", "ccm-editor-itemfilter"); + filterInput.setAttribute("id", "ccm-editor-assetfilter"); filterInput.setAttribute("type", "text"); const applyFiltersButton: Element = filterForm .appendChild(document.createElement("button")); @@ -223,3 +223,589 @@ export class InsertInternalLinkCommand extends CCMEditorCommand { this.button.setAttribute("disabled", "true"); } } + +export class InsertMediaAssetCommand extends CCMEditorCommand { + + private button: HTMLElement; + + private IMAGE_TYPE: string = "org.libreccm.assets.ImageAsset"; + private VIDEO_TYPE: string = "org.libreccm.assets.VideoAsset"; + private EXTERNAL_VIDEO_TYPE: string + = "org.libreccm.assets.ExternalVideoAsset"; + private AUDIO_TYPE: string = "org.libreccm.assets.Audio"; + private EXTERNAL_AUDIO_TYPE: string + = "org.libreccm.assets.ExternalAudioAsset"; + + constructor(editor: CCMEditor, settings: any) { + + super(editor, settings); + + this.button = this.fragment + .appendChild(document.createElement("button")); + + const icon: Element = this.button + .appendChild(document.createElement("i")); + icon.className = "fa fa-file-image-o"; + + const text: Element = this.button + .appendChild(document.createElement("span")); + this.button.setAttribute("title", + "Insert media content (image, audio, video)"); + text.textContent = `Insert media content like an image, a audio file or + a video`; + text.className = "ccm-editor-accessibility"; + + const command: InsertMediaAssetCommand = this; + + this.button.addEventListener("click", function() { + + event.preventDefault(); + + const currentRange: Range = document.getSelection().getRangeAt(0); + + const dialogFragment: DocumentFragment = document + .createDocumentFragment(); + const dialogElem: HTMLElement = dialogFragment + .appendChild(document.createElement("div")); + dialogElem.className = "ccm-editor-selectdialog"; + const dialogTitleElem: Element = dialogElem + .appendChild(document.createElement("h1")); + dialogTitleElem.textContent = "Insert media"; + + const closeButton: HTMLButtonElement = dialogElem + .appendChild(document.createElement("button")); + closeButton.className = "ccm-editor-selectdialog-closebutton"; + closeButton.textContent = "\u2715"; + closeButton.addEventListener("click", function() { + event.preventDefault(); + const bodyElem = document.getElementsByTagName("body").item(0); + bodyElem.removeChild(dialogElem); + document.getSelection().removeAllRanges(); + document.getSelection().addRange(currentRange); + }); + + const filterForm: HTMLElement = dialogElem + .appendChild(document.createElement("div")); + const contentSectionSelectLabel: HTMLElement = filterForm + .appendChild(document.createElement("label")); + contentSectionSelectLabel + .setAttribute("for", "ccm-editor-contentsection-select"); + contentSectionSelectLabel.textContent + = "Show media assets from Content Section"; + const contentSectionSelect: HTMLSelectElement = filterForm + .appendChild(document.createElement("select")); + contentSectionSelect + .setAttribute("id", "ccm-editor-contentsection-select"); + const typeSelectLabel: HTMLElement = filterForm + .appendChild(document.createElement("label")); + typeSelectLabel.setAttribute("for", "ccm-editor-assettype-select"); + typeSelectLabel.textContent = "Type"; + const typeSelect: HTMLSelectElement = filterForm + .appendChild(document.createElement("select")); + typeSelect.setAttribute("id", "ccm-editor-assettype-select"); + const typeSelectOptions = [ + { + type: command.IMAGE_TYPE, + label: "Image", + }, + { + type: command.VIDEO_TYPE, + label: "Video", + }, + { + type: command.EXTERNAL_VIDEO_TYPE, + label: "External Video", + }, + { + type: command.AUDIO_TYPE, + label: "Audio file", + }, + { + type: command.EXTERNAL_AUDIO_TYPE, + label: "External Audio file", + }, + ]; + for (const typeOption of typeSelectOptions) { + const option: HTMLElement = document.createElement("option"); + option.setAttribute("value", typeOption.type); + option.textContent = typeOption.label + typeSelect.appendChild(option); + } + const filterInputLabel: Element = filterForm + .appendChild(document.createElement("label")); + filterInputLabel.setAttribute("id", "ccm-editor-assetfilter"); + filterInputLabel.textContent = "Filter items"; + const filterInput: HTMLInputElement = filterForm + .appendChild(document.createElement("input")); + filterInput.setAttribute("id", "ccm-editor-assetfilter"); + filterInput.setAttribute("type", "text"); + const applyFiltersButton: Element = filterForm + .appendChild(document.createElement("button")); + applyFiltersButton.textContent = "Clear filters"; + + const clearFiltersButton: Element = filterForm + .appendChild(document.createElement("button")); + clearFiltersButton.textContent = "Clear filters"; + clearFiltersButton.addEventListener("click", function(event){ + event.preventDefault(); + filterInput.value = ""; + }); + + const table: HTMLElement = dialogElem + .appendChild(document.createElement("table")); + const tableHead: HTMLElement = table + .appendChild(document.createElement("thead")); + const headerRow: HTMLElement = tableHead + .appendChild(document.createElement("tr")); + const titleColHeader: HTMLElement = headerRow + .appendChild(document.createElement("th")); + const typeColHeader: Element = headerRow + .appendChild(document.createElement("th")); + const placeColHeader: Element = headerRow + .appendChild(document.createElement("th")); + titleColHeader.textContent = "Title"; + typeColHeader.textContent = "Type"; + placeColHeader.textContent = "Place"; + const tableBody: HTMLElement = table + .appendChild(document.createElement("tbody")); + + const contextPrefix = editor.getDataAttribute("content-prefix"); + // Get content sections + const currentSection = editor + .getDataAttribute("current-contentsection-primaryurl"); + const sectionsUrl = contextPrefix + "/content-sections/"; + fetch(sectionsUrl, { credentials: "include" }).then((response) => { + + if (response.ok) { + response.json().then((data) => { + for(const section of data) { + const option: HTMLElement = contentSectionSelect + .appendChild(document.createElement("option")); + option.setAttribute("value", section.primaryUrl); + option.textContent = section.primaryUrl; + if (section.primaryUrl === currentSection) { + option.setAttribute("selected", "selected"); + } + } + }); + } + }); + + contentSectionSelect.addEventListener("change", (event) => { + event.preventDefault(); + command.fetchAssets(dialogElem, + contentSectionSelect, + typeSelect, + filterInput, + tableBody, + currentRange); + }); + typeSelect.addEventListener("change", (event) => { + event.preventDefault(); + command.fetchAssets(dialogElem, + contentSectionSelect, + typeSelect, + filterInput, + tableBody, + currentRange); + }); + applyFiltersButton.addEventListener("click", (event) => { + event.preventDefault(); + command.fetchAssets(dialogElem, + contentSectionSelect, + typeSelect, + filterInput, + tableBody, + currentRange); + }); + }); + } + + private fetchAssets(dialogElem: HTMLElement, + contentSectionSelect: HTMLSelectElement, + typeSelect: HTMLSelectElement, + filterField: HTMLInputElement, + resultsTableBody: HTMLElement, + currentRange: Range): void { + + const assetsUrl = this.buildAssetsUrl(contentSectionSelect.value, + typeSelect.value, + filterField.value); + + fetch(assetsUrl, { credentials: "include" }).then((response) => { + + if (response.ok) { + response.json().then((data) => { + + resultsTableBody.innerHTML = ""; + + for(const asset of data) { + const row: HTMLElement = resultsTableBody + .appendChild(document.createElement("tr")); + const dataTitle: HTMLElement = row + .appendChild(document.createElement("td")); + const dataType: HTMLElement = row + .appendChild(document.createElement("td")); + const dataPlace = row + .appendChild(document.createElement("td")); + + const selectAssetButton = dataTitle + .appendChild(document.createElement("button")); + selectAssetButton.textContent = asset.title; + + selectAssetButton + .addEventListener("click", event => { + event.preventDefault(); + + this.insertMedia( + dialogElem, + this.editor + .getDataAttribute("content-prefix"), + contentSectionSelect.value, + asset.uuid, + asset.title, + asset.type, + asset.typeLabel, + currentRange); + }); + + dataType.textContent = asset.typeLabel; + dataType.textContent = asset.place; + } + }, + (failure) => { + const errorMsgElement = document.createElement("div"); + errorMsgElement.classList.add("ccm-editor-error"); + errorMsgElement.textContent = `Failed to fetch assets from + ${assetsUrl}: ${failure}`; + resultsTableBody.parentNode.parentNode + .insertBefore(errorMsgElement, + resultsTableBody.parentNode); + }); + } else { + const errorMsgElement = document.createElement("div"); + errorMsgElement.classList.add("ccm-editor-error"); + errorMsgElement.textContent = `Failed to fetch assets from + ${assetsUrl}. Status code: ${response.status}. + Status text: ${response.statusText}`; + resultsTableBody.parentNode.parentNode + .insertBefore(errorMsgElement, resultsTableBody.parentNode); + } + }, + (failure) => { + const errorMsgElement = document.createElement("div"); + errorMsgElement.classList.add("ccm-editor-error"); + errorMsgElement.textContent = `Failed to fetch assets from + ${assetsUrl}: ${failure}`; + resultsTableBody.parentNode.parentNode + .insertBefore(errorMsgElement, resultsTableBody.parentNode); + }); + } + + private buildAssetsUrl(contentSection: string, + type: string, + query?: string): string { + const contextPrefix = this.editor.getDataAttribute("content-prefix"); + + let url: string = `${contextPrefix}/content-sections`; + if (!(new RegExp("^/.*").test(contentSection))) { + url = `${url}/`; + } + url = `${url}${contentSection}`; + if (!(new RegExp(".*/$").test(url))) { + url = `${url}/` + } + if (query) { + url = `${url}?query=${query}`; + } + + return url; + } + + private insertMedia(selectDialogElem: HTMLElement, + contextPrefix: string, + contentSection: string, + assetUuid: string, + assetTitle: string, + assetType: string, + assetTypeLabel: string, + currentRange: Range): void { + + selectDialogElem.setAttribute("style", "display: none"); + + const dialogId: string = "ccm-editor-insertmedia"; + const captionFieldId: string = `${dialogId}-caption`; + const altFieldId: string = `${dialogId}-alt`; + const decorativeCheckboxId: string + = `${dialogId}-isdecorative`; + const widthFieldId: string = `${dialogId}-width`; + const heightFieldId: string = `${dialogId}-height`; + + const fragment: DocumentFragment = document.createDocumentFragment(); + const insertDialogElem: HTMLElement = fragment + .appendChild(document.createElement("div")); + insertDialogElem.classList.add("ccm-editor-dialog"); + insertDialogElem.id = dialogId; + + const headingElem: HTMLElement = insertDialogElem + .appendChild(document.createElement("h1")); + headingElem.textContent = `Insert media asset "${assetTitle}" of + type ${assetTypeLabel}`; + + const formElem: HTMLElement = insertDialogElem + .appendChild(document.createElement("form")); + + const captionLabel: HTMLLabelElement = formElem + .appendChild(document.createElement("label")); + captionLabel.htmlFor = captionFieldId; + captionLabel.textContent = "Caption"; + const captionField: HTMLInputElement = formElem + .appendChild(document.createElement("input")); + captionField.id = captionFieldId; + captionField.type = "text"; + + if (this.IMAGE_TYPE === assetType) { + + const altLabel: HTMLLabelElement = formElem + .appendChild(document.createElement("label")); + altLabel.htmlFor = altFieldId; + altLabel.textContent = "Alternativ text for image"; + const altField: HTMLInputElement = formElem + .appendChild(document.createElement("input")); + altField.type = "text"; + altField.id = altFieldId; + + const decorativeLabel: HTMLLabelElement = formElem + .insertBefore(document.createElement("label"), captionLabel); + decorativeLabel.htmlFor = decorativeCheckboxId; + decorativeLabel.textContent = "Is decorative image?"; + const decorativeCheckbox: HTMLInputElement = formElem + .insertBefore(document.createElement("input"), captionLabel); + decorativeCheckbox.id = decorativeCheckboxId; + decorativeCheckbox.type = "checkbox"; + + decorativeCheckbox.addEventListener("change", (event) => { + if (decorativeCheckbox.checked) { + captionLabel.setAttribute("style", "display: none"); + captionField.setAttribute("style", "display: none"); + captionField.value = ""; + + altLabel.setAttribute("style", "display: none"); + altField.setAttribute("style", "display: none"); + altField.value = ""; + } else { + captionLabel.removeAttribute("style"); + captionField.removeAttribute("style"); + altLabel.removeAttribute("style"); + altField.removeAttribute("style"); + } + }); + + if (this.IMAGE_TYPE === assetType + || this.VIDEO_TYPE === assetType + || this.EXTERNAL_VIDEO_TYPE === assetType) { + + const widthLabel: HTMLLabelElement = formElem + .appendChild(document.createElement("label")); + widthLabel.htmlFor = widthFieldId; + widthLabel.textContent = "Width"; + const widthField: HTMLInputElement = formElem + .appendChild(document.createElement("input")); + widthField.id = widthFieldId; + widthField.type = "number"; + + const heightLabel: HTMLLabelElement = formElem + .appendChild(document.createElement("label")); + heightLabel.htmlFor = heightFieldId; + heightLabel.textContent = "height"; + const heightField: HTMLInputElement = formElem + .appendChild(document.createElement("input")); + heightField.id = heightFieldId; + heightField.type = "number"; + } + + const insertButton: HTMLButtonElement = formElem + .appendChild(document.createElement("button")); + insertButton.textContent = "insert"; + insertButton.addEventListener("click", (event) => { + event.preventDefault(); + + const captionField: HTMLInputElement = document + .getElementById(captionFieldId) as HTMLInputElement; + let mediaHtml: string; + switch(assetType) { + case this.AUDIO_TYPE: { + mediaHtml = this + .generateAudioHtml("0", captionField.value); + } + case this.EXTERNAL_AUDIO_TYPE: { + mediaHtml = this + .generateAudioHtml("0", captionField.value); + } + case this.EXTERNAL_VIDEO_TYPE: { + const widthField: HTMLInputElement = document + .getElementById(widthFieldId) as HTMLInputElement; + const heightField: HTMLInputElement = document + .getElementById(widthFieldId) as + HTMLInputElement; + mediaHtml = this + .generateVideoHtml("0", + captionField.value, + Number(widthField.value), + Number(heightField.value)); + } + case this.IMAGE_TYPE: { + const altField: HTMLInputElement = document + .getElementById(altFieldId) as HTMLInputElement; + const widthField: HTMLInputElement = document + .getElementById(widthFieldId) as HTMLInputElement; + const heightField: HTMLInputElement = document + .getElementById(widthFieldId) as + HTMLInputElement; + const imageUrl: string = this + .generateImageUrl(contextPrefix, + contentSection, + assetUuid); + + if (decorativeCheckbox.checked) { + mediaHtml = this + .generateDecorativeImageHtml( + imageUrl, + Number(widthField.value), + Number(heightField.value)); + } else { + mediaHtml = this + .generateImageHtml(imageUrl, + captionField.value, + altField.value, + Number(widthField.value), + Number(heightField.value)); + } + } + case this.VIDEO_TYPE: { + const widthField: HTMLInputElement = document + .getElementById(widthFieldId) as HTMLInputElement; + const heightField: HTMLInputElement = document + .getElementById(widthFieldId) as + HTMLInputElement; + mediaHtml = this + .generateVideoHtml("0", + captionField.value, + Number(widthField.value), + Number(heightField.value)); + } + default: + mediaHtml = ""; + } + + document.getSelection().removeAllRanges(); + document.getSelection().addRange(currentRange); + document.execCommand("insertHTML", false, mediaHtml); + + const bodyElem = document.getElementsByTagName("body").item(0); + bodyElem.removeChild(insertDialogElem); + bodyElem.removeChild(selectDialogElem); + }); + + const cancelButton: HTMLButtonElement = formElem + .appendChild(document.createElement("button")); + cancelButton.textContent = "Cancel"; + cancelButton.addEventListener("click", (event) => { + event.preventDefault(); + + const bodyElem = document.getElementsByTagName("body").item(0); + bodyElem.removeChild(insertDialogElem); + selectDialogElem.removeAttribute("style"); + }); + } + } + + private generateDecorativeImageHtml(imageUrl: string, + width: number = -1, + height: number = -1): string { + + let dimensions: string; + if (width > 0) { + dimensions = `${dimensions} width="${width}"`; + } + if (height > 0) { + dimensions = `${dimensions} height="${height}"`; + } + + return ``; + } + + private generateImageHtml(imageUrl: string, + caption: string, + alt: string, + width: number = -1, + height: number = -1): string { + + let dimensions: string; + if (width > 0) { + dimensions = `${dimensions} width="${width}"`; + } + if (height > 0) { + dimensions = `${dimensions} height="${height}"`; + } + + return `
+ ${alt} +
${caption}
+
` + } + + private generateImageUrl(contextPrefix: string, + contentSection: string, + assetUuid: string, + width: number = -1, + height: number = -1): string { + + let imageUrl: string = `${contextPrefix}/content-sections`; + if (!(new RegExp("^/.*").test(contentSection))) { + imageUrl = `${imageUrl}/`; + } + imageUrl = `${imageUrl}{$contentSection}`; + if (!(new RegExp(".*/$").test(contentSection))) { + imageUrl = `${imageUrl}/`; + } + return `${imageUrl}/images/uuid-${assetUuid} + ?width=${width}&height=${height}`; + } + + private generateVideoHtml(videoUrl: string, + caption: string, + width: number = -1, + height: number = -1): string { + + return `
+ Not implemented yet +
${caption}
+
`; + } + + private generateAudioHtml(audioUrl: string, caption: string): string { + + return `
+ Not implemented yet +
${caption}
+
`; + } + + getCommandType(): CCMEditorCommandType { + return CCMEditorCommandType.INSERT_INLINE; + } + + selectionChanged(selection: Selection) { + + } + + enableCommand(): void { + this.button.removeAttribute("disabled"); + } + + disableCommand(): void { + this.button.setAttribute("disabled", "disabled"); + } + +}