diff --git a/EMS/admin-ui-bundle/assets/css/core/components/iframe_preview.scss b/EMS/admin-ui-bundle/assets/css/core/components/iframe_preview.scss new file mode 100644 index 000000000..359ac2f55 --- /dev/null +++ b/EMS/admin-ui-bundle/assets/css/core/components/iframe_preview.scss @@ -0,0 +1,4 @@ +iframe.styleset-preview { + border-width: 0; + width: 100%; +} diff --git a/EMS/admin-ui-bundle/assets/css/core/components/json_menu_nested.scss b/EMS/admin-ui-bundle/assets/css/core/components/json_menu_nested.scss index 5c7d178bc..ec36fe408 100644 --- a/EMS/admin-ui-bundle/assets/css/core/components/json_menu_nested.scss +++ b/EMS/admin-ui-bundle/assets/css/core/components/json_menu_nested.scss @@ -1,3 +1,4 @@ +@import "@fortawesome/fontawesome-free/scss/variables"; @import "../helpers/mixins"; .json-menu-nested-component { @@ -143,9 +144,9 @@ background-color: white; } - .jmn-dropdown { @include icon-after("\f0d7"); } - .jmn-dropdown-add { @include icon-before("\f067"); } - .jmn-dropdown-more { @include icon-before("\f142", 0); } + .jmn-dropdown { @include icon-after($fa-var-caret-down); } + .jmn-dropdown-add { @include icon-before($fa-var-plus); } + .jmn-dropdown-more { @include icon-before($fa-var-ellipsis-v, 0); } .jmn-btn-move { cursor: move; diff --git a/EMS/admin-ui-bundle/assets/css/core/helpers/mixins.scss b/EMS/admin-ui-bundle/assets/css/core/helpers/mixins.scss index 42116eca9..f26ecff59 100644 --- a/EMS/admin-ui-bundle/assets/css/core/helpers/mixins.scss +++ b/EMS/admin-ui-bundle/assets/css/core/helpers/mixins.scss @@ -1,20 +1,22 @@ -@mixin icon($unicode) { - font-family: FontAwesome, serif; +@import "@fortawesome/fontawesome-free/scss/functions"; + +@mixin icon($icon) { + font-family: $fa-style-family; font-weight: 400; - content: $unicode; + content: fa-content($icon); } -@mixin icon-before($unicode, $marginRight : 5px) { +@mixin icon-before($icon, $marginRight : 5px) { &::before { - @include icon($unicode); + @include icon($icon); margin-right: $marginRight; } } -@mixin icon-after($unicode, $marginLeft : 5px) { +@mixin icon-after($icon, $marginLeft : 5px) { &::after { - @include icon($unicode); + @include icon($icon); margin-left: $marginLeft; } diff --git a/EMS/admin-ui-bundle/assets/css/core/plugins/wysiwyg.scss b/EMS/admin-ui-bundle/assets/css/core/plugins/wysiwyg.scss new file mode 100644 index 000000000..1308db696 --- /dev/null +++ b/EMS/admin-ui-bundle/assets/css/core/plugins/wysiwyg.scss @@ -0,0 +1,28 @@ +@import "@fortawesome/fontawesome-free/scss/functions"; +@import "@fortawesome/fontawesome-free/scss/variables"; + +// Specific to dialog popup +.cke_dialog { + .select2-container { + z-index:10020; + position:relative !important; + + .select2-selection--single .select2-selection__rendered .fa { + font-family: $fa-style-family !important; + } + } + + div.cke_dialog_ui_input_select { + display: block; + } +} + +// More generic change, because dialog z-index is higher than select2 dropdown. +.select2-dropdown { + z-index: 10051 !important; + position: absolute !important; +} + +.cke_combopanel { + width:350px !important; +} diff --git a/EMS/admin-ui-bundle/assets/images/anonymous.gif b/EMS/admin-ui-bundle/assets/images/anonymous.gif new file mode 100644 index 000000000..9f458460a Binary files /dev/null and b/EMS/admin-ui-bundle/assets/images/anonymous.gif differ diff --git a/EMS/admin-ui-bundle/assets/images/background-img.png b/EMS/admin-ui-bundle/assets/images/background-img.png new file mode 100644 index 000000000..12bd7373d Binary files /dev/null and b/EMS/admin-ui-bundle/assets/images/background-img.png differ diff --git a/EMS/admin-ui-bundle/assets/images/ball.svg b/EMS/admin-ui-bundle/assets/images/ball.svg new file mode 100644 index 000000000..4481a53b6 --- /dev/null +++ b/EMS/admin-ui-bundle/assets/images/ball.svg @@ -0,0 +1 @@ + diff --git a/EMS/admin-ui-bundle/assets/images/big-logo.png b/EMS/admin-ui-bundle/assets/images/big-logo.png new file mode 100644 index 000000000..75f4cc827 Binary files /dev/null and b/EMS/admin-ui-bundle/assets/images/big-logo.png differ diff --git a/EMS/admin-ui-bundle/assets/images/elasticms_ball_only_circlegrey.png b/EMS/admin-ui-bundle/assets/images/elasticms_ball_only_circlegrey.png new file mode 100644 index 000000000..1fd6033e0 Binary files /dev/null and b/EMS/admin-ui-bundle/assets/images/elasticms_ball_only_circlegrey.png differ diff --git a/EMS/admin-ui-bundle/assets/images/elasticms_ball_only_circlewhite.png b/EMS/admin-ui-bundle/assets/images/elasticms_ball_only_circlewhite.png new file mode 100644 index 000000000..aec24bd03 Binary files /dev/null and b/EMS/admin-ui-bundle/assets/images/elasticms_ball_only_circlewhite.png differ diff --git a/EMS/admin-ui-bundle/assets/images/logo.png b/EMS/admin-ui-bundle/assets/images/logo.png new file mode 100644 index 000000000..9714378b3 Binary files /dev/null and b/EMS/admin-ui-bundle/assets/images/logo.png differ diff --git a/EMS/admin-ui-bundle/assets/js/core/components/iframePreview.js b/EMS/admin-ui-bundle/assets/js/core/components/iframePreview.js new file mode 100644 index 000000000..a29487fb3 --- /dev/null +++ b/EMS/admin-ui-bundle/assets/js/core/components/iframePreview.js @@ -0,0 +1,58 @@ +import '../../../css/core/components/iframe_preview.scss' + +export class IframePreview { + constructor (iframe) { + this.iframe = iframe + const self = this + window.addEventListener('message', function (event) { + self.onMessage(event) + }) + } + + onMessage (event) { + if (event.source !== this.iframe.contentWindow) { + return + } + + if (event.data === 'ready') { + this.loadBody() + } else if (event.data === 'resize') { + this.adjustHeight() + } else { + console.log('Unknown event type: ' + event.data) + } + } + + loadBody () { + let body = this.iframe.getAttribute('data-iframe-body') + body = this.#changeSelfTargetLinksToParent(body) + const window = this.iframe.contentWindow || this.iframe.contentDocument.defaultView + window.postMessage(body, '*') + } + + adjustHeight () { + const window = this.iframe.contentWindow || this.iframe.contentDocument.defaultView + + let height = window.document.documentElement.scrollHeight; + + ['border-top-width', 'border-bottom-width', 'padding-top', 'padding-bottom'].forEach((v) => { + height += parseInt(window.getComputedStyle(this.iframe, null).getPropertyValue(v).replace('px', ''), 10) + }) + + this.iframe.height = height + } + + #changeSelfTargetLinksToParent (body) { + const parser = new DOMParser() + const dom = parser.parseFromString(body, 'text/html') + ;[...dom.getElementsByTagName('a')].forEach((link) => { + if (!link.getAttribute('target')) { + link.setAttribute('target', '_parent') + } + }) + + return dom.documentElement.outerHTML + } +} + +export default IframePreview diff --git a/EMS/admin-ui-bundle/assets/js/core/components/revisionTask.js b/EMS/admin-ui-bundle/assets/js/core/components/revisionTask.js new file mode 100644 index 000000000..d96e0d70f --- /dev/null +++ b/EMS/admin-ui-bundle/assets/js/core/components/revisionTask.js @@ -0,0 +1,164 @@ +import ajaxModal from '../../../js/core/helpers/ajaxModal' +import Sortable from 'sortablejs' + +export default class RevisionTask { + constructor () { + this.dashboard() + + this.tasksTab = document.querySelector('#tab_tasks') + if (this.tasksTab !== null) { + this.revisionTasks = document.querySelector('div#revision-tasks') + this.revisionTaskLoading = this.revisionTasks.querySelector('div#revision-tasks-loading') + this.revisionTasksContent = this.revisionTasks.querySelector('div#revision-tasks-content') + this._addClickListeners() + this.loadTasks() + } + } + + dashboard () { + document.addEventListener('click', (e) => { + if (e.target.classList.contains('task-modal')) { + e.preventDefault() + ajaxModal.load({ url: e.target.dataset.url, title: e.target.dataset.title }) + } + if (e.target.classList.contains('btn-task-change-owner-modal')) { + e.preventDefault() + ajaxModal.load( + { url: e.target.dataset.url, title: e.target.dataset.title }, + (json) => { + if (Object.prototype.hasOwnProperty.call(json, 'modalSuccess') && json.modalSuccess === true) { + window.location.reload() + } + } + ) + } + }) + } + + loadTasks () { + fetch(this.revisionTasks.dataset.url, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }) + .then((response) => { return response.json() }) + .then((json) => { + if (Object.prototype.hasOwnProperty.call(json, 'tab')) this._updateTab(json.tab) + }) + } + + _updateTab (tab) { + this.revisionTasksContent.outerHTML = tab + this.revisionTasksContent = this.revisionTasks.querySelector('div#revision-tasks-content') + this.revisionTaskLoading.style.display = 'none' + this.revisionTasksContent.style.display = 'block' + } + + _addClickListeners () { + this.tasksTab.addEventListener('click', (event) => { + const target = event.target + + if (target.classList.contains('btn-task-modal')) this._onClickButtonTaskCreateOrUpdate(target) + if (target.classList.contains('btn-task-handle')) this._onClickButtonHandle(target) + if (target.id === 'btn-tasks-reorder') this._onClickButtonTaskReorder(target) + if (target.id === 'btn-tasks-approved') this._onClickButtonTasksApproved(event, target) + + const closestTasksItem = target.closest('.tasks-item') + if (closestTasksItem || target.classList.contains('tasks-item')) { + this._onClickTaskItem(event, closestTasksItem ?? target) + } + }, true) + document.addEventListener('click', (event) => { + const target = event.target + if (target.id === 'btn-task-delete') this._onClickButtonTaskDelete(target) + }) + } + + _onClickButtonTaskCreateOrUpdate (button) { + ajaxModal.load({ url: button.dataset.url, title: button.dataset.title }, (json) => { + if (Object.prototype.hasOwnProperty.call(json, 'modalSuccess') && json.modalSuccess) { + this.loadTasks() + } + }) + } + + _onClickButtonTaskDelete (button) { + ajaxModal.load({ url: button.dataset.url, title: button.dataset.title }, (json) => { + if (Object.prototype.hasOwnProperty.call(json, 'modalSuccess') && json.modalSuccess) { + this.loadTasks() + } + }) + } + + _onClickButtonHandle (button) { + const formData = new FormData(this.tasksTab.querySelector('form')) + formData.set('handle', button.dataset.type) + + fetch(this.revisionTasks.dataset.url, { method: 'POST', body: formData }) + .then((response) => response.json()) + .then((json) => { + if (Object.prototype.hasOwnProperty.call(json, 'success') && json.success) this.loadTasks() + if (Object.prototype.hasOwnProperty.call(json, 'tab')) this._updateTab(json.tab) + }) + } + + _onClickButtonTaskReorder (button) { + this.tasksTab.classList.add('reorder') + button.style.display = 'none' + + const btnReorderCancel = this.tasksTab.querySelector('#btn-tasks-reorder-cancel') + const btnReorderSave = this.tasksTab.querySelector('#btn-tasks-reorder-save') + + btnReorderSave.style.display = 'inline-block' + btnReorderCancel.style.display = 'inline-block' + btnReorderCancel.removeAttribute('disabled') + + const tasksPlannedList = this.tasksTab.querySelector('ul#revision-tasks-planned-list') + tasksPlannedList.querySelectorAll('.tasks-item').item(0).classList.remove('tasks-item-current') + + Sortable.create(tasksPlannedList, { + fallbackTolerance: 3, + animation: 150, + ghostClass: 'dragging' + }) + + const finishReorder = () => { + this.loadTasks() + this.tasksTab.classList.remove('reorder') + } + + btnReorderCancel.onclick = () => finishReorder() + btnReorderSave.onclick = () => { + const taskIds = [] + tasksPlannedList.querySelectorAll('.tasks-item').forEach((item) => { + taskIds.push(item.dataset.id) + }) + fetch(btnReorderSave.dataset.url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ taskIds }) + }).finally(() => finishReorder()) + } + } + + _onClickButtonTasksApproved (event, button) { + event.preventDefault() + const btnText = button.textContent + const toggleText = button.dataset.toggleText + const list = this.tasksTab.querySelector('ul#revision-tasks-approved') + + button.dataset.toggleText = btnText + button.innerHTML = toggleText + if (button.dataset.toggle === 'true') { + list.style.display = 'block' + button.dataset.toggle = 'false' + } else { + list.style.display = 'none' + button.dataset.toggle = 'true' + } + } + + _onClickTaskItem (event, link) { + event.preventDefault() + ajaxModal.load({ url: link.dataset.url, title: link.dataset.title }) + } +} diff --git a/EMS/admin-ui-bundle/assets/js/core/core.js b/EMS/admin-ui-bundle/assets/js/core/core.js index 70b9fcfd7..47131e016 100644 --- a/EMS/admin-ui-bundle/assets/js/core/core.js +++ b/EMS/admin-ui-bundle/assets/js/core/core.js @@ -4,7 +4,9 @@ import CodeEditor from './plugins/codeEditor' import CollapsibleCollection from './plugins/collapsibleCollection' import Datatable from './plugins/datatable' import File from './plugins/file' +import Iframe from './plugins/iframe' import Image from './plugins/image' +import Job from './plugins/job' import JsonMenuNested from './plugins/jsonMenuNested' import MediaLibrary from './plugins/mediaLibrary' import NestedSortable from './plugins/nestedSortable' @@ -15,8 +17,13 @@ import SymfonyCollection from './plugins/symfonyCollection' import Tooltip from './plugins/tooltip' import WYSIWYG from './plugins/wysiwyg' +import RevisionTask from './components/revisionTask' + +import { EMS_ADDED_DOM_EVENT } from './events/addedDomEvent' + class Core { constructor () { + this._statusUpdateUrl = document.body.getAttribute('data-status-url') this._domListeners = [ new Button(), new Choice(), @@ -24,7 +31,9 @@ class Core { new CollapsibleCollection(), new Datatable(), new File(), + new Iframe(), new Image(), + new Job(), new JsonMenuNested(), new MediaLibrary(), new NestedSortable(), @@ -35,8 +44,8 @@ class Core { new Tooltip(), new WYSIWYG() ] - document.addEventListener('emsAddedDomEvent', (event) => this.load(event.target)) - this.documentReady() + document.addEventListener(EMS_ADDED_DOM_EVENT, (event) => this.load(event.target)) + this.coreReady() } load (target) { @@ -47,12 +56,46 @@ class Core { this._domListeners.forEach((element) => element.load(target)) } - documentReady () { + coreReady () { if (document.readyState === 'complete' || document.readyState === 'interactive') { setTimeout(this.load(document), 1) } else { document.addEventListener('DOMContentLoaded', this.load(document)) } + this.initStatusRefresh() + this.components = [ + new RevisionTask() + ] + } + + initStatusRefresh () { + const self = this + window.setInterval(function () { + self.updateStatus() + }, 180000) + } + + updateStatus () { + const xhr = new XMLHttpRequest() + xhr.open('GET', this._statusUpdateUrl, true) + + xhr.onreadystatechange = function (event) { + if (this.readyState !== 4) { + return + } + + const statusLink = document.getElementById('status-overview') + if (this.status === 200) { + const json = JSON.parse(xhr.responseText) + statusLink.innerHTML = json.body + statusLink.setAttribute('title', json.title) + } else { + statusLink.setAttribute('title', `Error ${xhr.status}`) + statusLink.innerHTML = `Error ${xhr.status}` + } + } + + xhr.send() } } diff --git a/EMS/admin-ui-bundle/assets/js/core/events/addedDomEvent.js b/EMS/admin-ui-bundle/assets/js/core/events/addedDomEvent.js index c008b1066..14172191d 100644 --- a/EMS/admin-ui-bundle/assets/js/core/events/addedDomEvent.js +++ b/EMS/admin-ui-bundle/assets/js/core/events/addedDomEvent.js @@ -1,6 +1,7 @@ +export const EMS_ADDED_DOM_EVENT = 'emsAddedDomEvent' export class AddedDomEvent { constructor (target) { - this._event = new CustomEvent('emsAddedDomEvent', { target }) + this._event = new CustomEvent(EMS_ADDED_DOM_EVENT, { target }) this._target = target } @@ -8,3 +9,5 @@ export class AddedDomEvent { document.dispatchEvent(this._event) } } + +export default AddedDomEvent diff --git a/EMS/admin-ui-bundle/assets/js/core/events/changeEvent.js b/EMS/admin-ui-bundle/assets/js/core/events/changeEvent.js new file mode 100644 index 000000000..bdcfbb13f --- /dev/null +++ b/EMS/admin-ui-bundle/assets/js/core/events/changeEvent.js @@ -0,0 +1,12 @@ +export const EMS_CHANGE_EVENT = 'emsChangeEvent' +export class ChangeEvent { + constructor (input) { + this._event = new CustomEvent(EMS_CHANGE_EVENT, { input }) + this._input = input + } + + dispatch () { + document.dispatchEvent(this._event) + } +} +export default ChangeEvent diff --git a/EMS/admin-ui-bundle/assets/js/core/helpers/queryString.js b/EMS/admin-ui-bundle/assets/js/core/helpers/queryString.js new file mode 100644 index 000000000..3bbdfa4ac --- /dev/null +++ b/EMS/admin-ui-bundle/assets/js/core/helpers/queryString.js @@ -0,0 +1,23 @@ +export function queryString () { + // This function is anonymous, is executed immediately and + // the return value is assigned to QueryString! + const queryString = {} + const query = window.location.search.substring(1) + const vars = query.split('&') + for (let i = 0; i < vars.length; i++) { + const pair = vars[i].split('=') + // If first entry with this name + if (typeof queryString[pair[0]] === 'undefined') { + queryString[pair[0]] = decodeURIComponent(pair[1]) + // If second entry with this name + } else if (typeof queryString[pair[0]] === 'string') { + queryString[pair[0]] = [queryString[pair[0]], decodeURIComponent(pair[1])] + // If third or later entry with this name + } else { + queryString[pair[0]].push(decodeURIComponent(pair[1])) + } + } + return queryString +} + +export default queryString diff --git a/EMS/admin-ui-bundle/assets/js/core/plugins/codeEditor.js b/EMS/admin-ui-bundle/assets/js/core/plugins/codeEditor.js index cfcb3a6f2..aa9d4ef25 100644 --- a/EMS/admin-ui-bundle/assets/js/core/plugins/codeEditor.js +++ b/EMS/admin-ui-bundle/assets/js/core/plugins/codeEditor.js @@ -3,6 +3,12 @@ import ace from 'ace-builds/src-noconflict/ace' import 'ace-builds/webpack-resolver' export default class CodeEditor { load (target) { + this.loadEditors(target) + this.loadAceThemePickers(target) + this.loadAceModePickers(target) + } + + loadEditors (target) { const self = this const codeEditors = target.getElementsByClassName('ems-code-editor') for (let i = 0; i < codeEditors.length; i++) { @@ -33,7 +39,6 @@ export default class CodeEditor { minLines = hiddenField.data('min-lines') } - console.log(theme) const editor = ace.edit(pre, { mode: language, readOnly: disabled, @@ -86,4 +91,45 @@ export default class CodeEditor { console.log(this.aceConfig) return this.aceConfig } + + getModules (startingWith) { + const filteredModule = [] + const modules = ace.config.all().$moduleUrls + for (const [key] of Object.entries(modules)) { + if (!key.startsWith(startingWith)) { + continue + } + let caption = key.substring(startingWith.length).replaceAll('_', ' ') + caption = caption.charAt(0).toUpperCase() + caption.slice(1) + filteredModule.push({ + id: key, + text: caption + }) + } + return filteredModule + } + + loadAceThemePickers (target) { + const codeEditorThemeField = $(target).find('.code_editor_theme_ems') + if (codeEditorThemeField.length === 0) { + return + } + const modes = this.getModules('ace/theme/') + codeEditorThemeField.select2({ + data: modes, + placeholder: 'Select a theme' + }) + } + + loadAceModePickers (target) { + const codeEditorModeField = $(target).find('.code_editor_mode_ems') + if (codeEditorModeField.length === 0) { + return + } + const modes = this.getModules('ace/mode/') + codeEditorModeField.select2({ + data: modes, + placeholder: 'Select a language' + }) + } } diff --git a/EMS/admin-ui-bundle/assets/js/core/plugins/iframe.js b/EMS/admin-ui-bundle/assets/js/core/plugins/iframe.js new file mode 100644 index 000000000..2a2736eae --- /dev/null +++ b/EMS/admin-ui-bundle/assets/js/core/plugins/iframe.js @@ -0,0 +1,13 @@ +import IframePreview from '../components/iframePreview' +class Iframe { + #iframes = [] + + load (target) { + const iframes = target.querySelectorAll('iframe[data-iframe-body]') + for (let i = 0; i < iframes.length; i++) { + this.#iframes.push(new IframePreview(iframes[i])) + } + } +} + +export default Iframe diff --git a/EMS/admin-ui-bundle/assets/js/core/plugins/job.js b/EMS/admin-ui-bundle/assets/js/core/plugins/job.js new file mode 100644 index 000000000..4391707d4 --- /dev/null +++ b/EMS/admin-ui-bundle/assets/js/core/plugins/job.js @@ -0,0 +1,36 @@ +import $ from 'jquery' + +class Job { + load (target) { + this.loadStartJob(target) + this.loadRequestJob(target) + } + + loadStartJob (target) { + $(target).find('[data-start-job-url]').each(function () { + $.ajax({ + type: 'POST', + url: this.getAttribute('data-start-job-url') + }).always(function () { + location.reload() + }) + }) + } + + loadRequestJob (target) { + $(target).find('a.request_job').on('click', function (e) { + e.preventDefault() + window.ajaxRequest.post($(e.target).data('url')) + .success(function (message) { + window.ajaxRequest.post(message.jobUrl) + $('ul#commands-log').prepend('