Add layout designer and welcome page templates with embed support
- Introduced a new layout designer page template that supports an embeddable mode for integration within iframes in external applications. This includes hiding CMS navigation and sending postMessage events to the parent window. - Added a new welcome page template that serves as an onboarding guide for users, featuring step-by-step instructions for connecting displays, uploading content, designing layouts, and scheduling content. - Included CSS styles for both templates to enhance the user interface and experience. - Implemented JavaScript functionality for live statistics on the welcome page, fetching counts for displays, media files, layouts, and schedules.
This commit is contained in:
580
ots-signs/views/layout-designer-page.twig
Normal file
580
ots-signs/views/layout-designer-page.twig
Normal file
@@ -0,0 +1,580 @@
|
||||
{#
|
||||
/**
|
||||
* OTS Signs — Layout Designer Page (with embed mode support)
|
||||
*
|
||||
* Overrides the Xibo core layout-designer-page.twig to add an embeddable
|
||||
* layout editor mode for use inside an iframe in external applications.
|
||||
*
|
||||
* Usage:
|
||||
* Normal: /layout/designer/{layoutId}
|
||||
* Embed: /layout/designer/{layoutId}?embed=1
|
||||
*
|
||||
* In embed mode:
|
||||
* - All CMS navigation is hidden (sidebar, topbar, help pane)
|
||||
* - Back/Exit buttons in the editor toolbar are hidden
|
||||
* - postMessage events are sent to the parent window:
|
||||
* xibo:editor:ready, xibo:editor:save, xibo:editor:publish, xibo:editor:exit
|
||||
* - Parent can send: xibo:editor:requestSave
|
||||
*
|
||||
* Copyright (C) 2020-2026 Xibo Signage Ltd
|
||||
* Copyright (C) 2026 Oribi Technology Services
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0
|
||||
*/
|
||||
#}
|
||||
{% extends "authed.twig" %}
|
||||
{% import "inline.twig" as inline %}
|
||||
|
||||
{% block title %}{{ "Layout Editor"|trans }} | {% endblock %}
|
||||
|
||||
{% set hideNavigation = "1" %}
|
||||
{% set forceHide = true %}
|
||||
|
||||
{% block headContent %}
|
||||
{{ parent() }}
|
||||
|
||||
{# Embed mode: early body class to prevent FOUC #}
|
||||
<script nonce="{{ cspNonce }}">
|
||||
(function(){
|
||||
try {
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
var isEmbed = params.get('embed') === '1' || window !== window.parent;
|
||||
if (isEmbed) {
|
||||
document.documentElement.classList.add('ots-embed-mode');
|
||||
}
|
||||
} catch(e) {
|
||||
// Cross-origin iframe detection may throw — treat as embed
|
||||
document.documentElement.classList.add('ots-embed-mode');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<style nonce="{{ cspNonce }}">
|
||||
/* ── Embed mode styles ──────────────────────────────────── */
|
||||
|
||||
/* Hide Back/Exit button area */
|
||||
.ots-embed-mode .back-button {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Hide the help pane */
|
||||
.ots-embed-mode #help-pane {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Hide floating page actions (notification bell, user menu) */
|
||||
.ots-embed-mode .ots-page-actions {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Remove content wrapper padding/margins for full-bleed editor */
|
||||
.ots-embed-mode #content-wrapper {
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.ots-embed-mode .page-content {
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.ots-embed-mode .page-content > .row {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.ots-embed-mode .page-content > .row > .col-sm-12 {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* Full viewport height for the editor */
|
||||
.ots-embed-mode body,
|
||||
.ots-embed-mode #layout-editor {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Hide the top-bar options dropdown (Publish/Checkout/Discard/etc.) */
|
||||
.ots-embed-mode .editor-top-bar .editor-options-dropdown {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Hide the editor jump list (layout switcher) */
|
||||
.ots-embed-mode .editor-top-bar #layoutJumpListContainer {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block pageContent %}
|
||||
|
||||
<!-- Editor structure -->
|
||||
<div id="layout-editor" data-published-layout-id="{{ publishedLayoutId }}" data-layout-id="{{ layout.layoutId }}" data-layout-help={{ help }}></div>
|
||||
|
||||
<div class="loading-overlay">
|
||||
<i class="fa fa-spinner fa-spin loading-icon"></i>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block javaScript %}
|
||||
{# Add common files #}
|
||||
{% include "editorTranslations.twig" %}
|
||||
{% include "editorVars.twig" %}
|
||||
|
||||
<script src="{{ theme.rootUri() }}dist/layoutEditor.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
|
||||
<script src="{{ theme.rootUri() }}dist/playlistEditor.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
|
||||
<script src="{{ theme.rootUri() }}dist/codeEditor.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
|
||||
<script src="{{ theme.rootUri() }}dist/wysiwygEditor.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
|
||||
<script src="{{ theme.rootUri() }}dist/editorCommon.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
|
||||
|
||||
<script type="text/javascript" nonce="{{ cspNonce }}">
|
||||
var previewJwt = "{{ previewJwt }}";
|
||||
|
||||
{% autoescape "js" %}
|
||||
{# Custom translations #}
|
||||
var layoutEditorHelpLink = "{{ help }}";
|
||||
|
||||
var layoutEditorTrans = {
|
||||
back: "{% trans "Back" %}",
|
||||
exit: "{% trans "Exit" %}",
|
||||
cancel: "{% trans "Cancel" %}",
|
||||
toggleFullscreen: "{% trans "Toggle Fullscreen Mode" %}",
|
||||
layerManager: "{% trans "Layer Manager" %}",
|
||||
snapToGrid: "{% trans "Snap to Grid" %}",
|
||||
snapToBorders: "{% trans "Snap to Borders" %}",
|
||||
snapToElements: "{% trans "Snap to Elements" %}",
|
||||
newTitle: "{% trans "New" %}",
|
||||
publishTitle: "{% trans "Publish" %}",
|
||||
discardTitle: "{% trans "Discard draft" %}",
|
||||
deleteTitle: "{% trans "Delete" %}",
|
||||
publishMessage: "{% trans "Are you sure you want to publish this Layout? If it is already in use the update will automatically get pushed." %}",
|
||||
checkoutTitle: "{% trans "Checkout" %}",
|
||||
scheduleTitle: "{% trans "Schedule" %}",
|
||||
clearLayout: "{% trans "Clear Canvas" %}",
|
||||
unlockTitle: "{% trans "Unlock" %}",
|
||||
saveTemplateTitle: "{% trans "Save Template" %}",
|
||||
readOnlyModeTitle: "{% trans "Read Only" %}",
|
||||
readOnlyModeMessage: "{% trans "You are viewing this Layout in read only mode, checkout by clicking on this message or from the Options menu above!" %}",
|
||||
lockedModeTitle: "{% trans "Locked" %}",
|
||||
lockedModeMessage: "{% trans "This is being locked by another user. Lock expires on: [expiryDate]" %}",
|
||||
checkoutMessage: "{% trans "Not editable, please checkout!" %}",
|
||||
unlockMessage: "{% trans "The current layout will be unlocked to other users. You will also be redirected to the Layouts page" %}",
|
||||
viewModeTitle: "{% trans "View" %}",
|
||||
actions: "{% trans "Actions" %}",
|
||||
welcomeModalMessage: "{% trans "This is published and cannot be edited. You can checkout for editing below, or continue to view it in a read only mode." %}",
|
||||
showingSampleData: "{% trans "Showing sample data" %}",
|
||||
emptyElementData: "{% trans "Has empty data" %}"
|
||||
};
|
||||
|
||||
var viewerTrans = {
|
||||
inlineEditor: "{% trans "Inline Editor" %}",
|
||||
nextWidget: "{% trans "Next widget" %}",
|
||||
previousWidget: "{% trans "Previous widget" %}",
|
||||
addWidget: "{% trans "Add Widget" %}",
|
||||
editGroup: "{% trans "Edit Group" %}",
|
||||
editPlaylist: "{% trans "Edit Playlist" %}",
|
||||
prev: '{{ "Previous Widget"|trans }}',
|
||||
next: '{{ "Next Widget"|trans }}',
|
||||
empty: '{{ "Empty Playlist"|trans }}',
|
||||
invalidRegion: '{{ "Invalid Region"|trans }}',
|
||||
editPlaylistTitle: '{{ "Edit Playlist"|trans }}',
|
||||
dynamicPlaylistTitle: '{{ "Dynamic Playlist"|trans }}'
|
||||
};
|
||||
|
||||
var timelineTrans = {
|
||||
zoomIn: "{% trans "Zoom in" %}",
|
||||
zoomOut: "{% trans "Zoom out" %}",
|
||||
resetZoom: "{% trans "Reset zoom" %}",
|
||||
zoomDelta: "{% trans "Visible area time span" %}",
|
||||
hiddenTimeruler: "{% trans "Zoom out to see timeruler!" %}",
|
||||
emptyTimeline: "{% trans "No Regions: Add a Region to start creating content by clicking here or the Edit Layout icon below!" %}",
|
||||
zoomFindSelected: "{% trans "Scroll to selected widget" %}",
|
||||
startTime: "{% trans "Visible area start time" %}",
|
||||
endTime: "{% trans "Visible area end time" %}",
|
||||
layoutName: "{% trans "Layout name" %}",
|
||||
layoutDuration: "{% trans "Layout duration" %}",
|
||||
layoutDimensions: "{% trans "Layout dimensions" %}",
|
||||
addToThisPosition: "{% trans "Add to this position" %}",
|
||||
hiddenContentInWidget: "{% trans "Zoom in to see more details!" %}",
|
||||
editRegion: "{% trans "Edit region" %}",
|
||||
openRegionAsPlaylist: "{% trans "Open as playlist" %}",
|
||||
widgetActions: "{% trans "Widget Actions:" %}",
|
||||
regionActions: "{% trans "Region Actions:" %}"
|
||||
};
|
||||
|
||||
var bottombarTrans = {
|
||||
edit: "{% trans "Edit layout regions" %}",
|
||||
addRegion: "{% trans "Add" %}",
|
||||
addRegionDesc: "{% trans "Add a new region" %}",
|
||||
deleteRegion: "{% trans "Delete region" %}",
|
||||
undo: "{% trans "Undo" %}",
|
||||
undoDesc: "{% trans "Revert last change" %}",
|
||||
close: "{% trans "Close" %}",
|
||||
closeDesc: "{% trans "Return to Layout View" %}",
|
||||
save: "{% trans "Save" %}",
|
||||
saveDesc: "{% trans "Save changes" %}",
|
||||
backToLayout: "{% trans "Go back to Layout view" %}",
|
||||
saveEditorChanges: "{% trans "Save editor changes" %}",
|
||||
playPreviewLayout: "{% trans "Play Layout preview" %}",
|
||||
playPreviewLayoutPOTitle: "{% trans "Preview stopped!" %}",
|
||||
playPreviewLayoutPOMessage: "{% trans "Click to Play again" %}",
|
||||
editLayout: "{% trans "Edit Layout" %}",
|
||||
stopPreviewLayout: "{% trans "Stop Layout preview" %}",
|
||||
nextWidget: "{% trans "Next widget" %}",
|
||||
previousWidget: "{% trans "Previous widget" %}",
|
||||
widgetName: "{% trans "Widget Name" %}",
|
||||
widgetType: "{% trans "Widget Type" %}",
|
||||
widgetTemplate: "{% trans "Widget Template Name" %}",
|
||||
elementName: "{% trans "Element Name" %}",
|
||||
elementMediaInfoName: "{{ "Media Name" |trans }}",
|
||||
elementMediaInfoId: "{{ "Media ID" |trans }}",
|
||||
elementGroupName: "{% trans "Element Group Name" %}",
|
||||
regionName: "{% trans "Region Name" %}",
|
||||
templateName: "{% trans "Template" %}",
|
||||
objectType: {
|
||||
layout: "{{ "Layout" |trans }}",
|
||||
region: "{{ "Region" |trans }}",
|
||||
zone: "{{ "Zone" |trans }}",
|
||||
playlist: "{{ "Playlist" |trans }}",
|
||||
widget: "{{ "Widget" |trans }}",
|
||||
element: "{{ "Element" |trans }}",
|
||||
"element-group": "{{ "Element Group" |trans }}"
|
||||
},
|
||||
tools: {
|
||||
audio: {
|
||||
name: "{{ "Audio" |trans }}",
|
||||
description: "{{ "Upload Audio files to assign to Widgets"|trans }}"
|
||||
},
|
||||
transitionIn: {
|
||||
name: "{{ "Transition In" |trans }}",
|
||||
description: "{{ "Apply a Transition type for the start of a media item"|trans }}"
|
||||
},
|
||||
transitionOut: {
|
||||
name: "{{ "Transition Out" |trans }}",
|
||||
description: "{{ "Apply a Transition type for the end of a media item"|trans }}"
|
||||
},
|
||||
permissions: {
|
||||
name: "{{ "Sharing" |trans }}",
|
||||
description: "{{ "Set View, Edit and Delete Sharing for Widgets and Playlists"|trans }}"
|
||||
}
|
||||
}
|
||||
};
|
||||
{% endautoescape %}
|
||||
</script>
|
||||
|
||||
<script type="text/javascript" nonce="{{ cspNonce }}">
|
||||
/**
|
||||
* Setup the background form.
|
||||
*/
|
||||
function backGroundFormSetup(dialog) {
|
||||
var $backgroundImageId = $('[name="backgroundImageId"]', dialog);
|
||||
var notFoundIcon = $('#bg_not_found_icon', dialog);
|
||||
var bgImageFileName = $('#bg_media_name', dialog);
|
||||
var saveButton = $('button#save', dialog);
|
||||
var initialBackgroundImageId = $backgroundImageId.val();
|
||||
var backgroundChanged = false;
|
||||
var mediaName = '';
|
||||
|
||||
function backgroundImageChange() {
|
||||
var id = $backgroundImageId.val();
|
||||
var isNotDefined = [0, ''].indexOf(id) !== -1;
|
||||
|
||||
$('#backgroundRemoveButton').toggleClass('disabled', isNotDefined);
|
||||
|
||||
if (isNotDefined) {
|
||||
notFoundIcon.show();
|
||||
bgImageFileName.hide();
|
||||
} else {
|
||||
notFoundIcon.hide();
|
||||
bgImageFileName.show();
|
||||
|
||||
if(mediaName) {
|
||||
bgImageFileName.html(mediaName);
|
||||
}
|
||||
|
||||
if (id !== initialBackgroundImageId) {
|
||||
saveButton.trigger('click');
|
||||
}
|
||||
}
|
||||
|
||||
if (id !== initialBackgroundImageId) {
|
||||
backgroundChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
function backgroundImageHandleDrop(mediaToAdd, fromProvider) {
|
||||
if(fromProvider) {
|
||||
lD.importFromProvider([mediaToAdd]).then((res) => {
|
||||
$backgroundImageId.val(res[0]).trigger('change');
|
||||
}).catch(function() {
|
||||
toastr.error(errorMessagesTrans.importingMediaFailed);
|
||||
});
|
||||
} else {
|
||||
$backgroundImageId.val(mediaToAdd).trigger('change');
|
||||
}
|
||||
|
||||
lD.toolbar.deselectCardsAndDropZones();
|
||||
}
|
||||
|
||||
$backgroundImageId.change(backgroundImageChange);
|
||||
backgroundImageChange();
|
||||
|
||||
$('#backgroundUploadButton').on('click', function(e) {
|
||||
layoutEditBackgroundButtonClicked(e, dialog);
|
||||
});
|
||||
|
||||
$('#backgroundRemoveButton').on('click', function(e) {
|
||||
if(!$(this).hasClass('disabled')) {
|
||||
$backgroundImageId.val('').trigger('change');
|
||||
}
|
||||
});
|
||||
|
||||
$('.background-image-add').droppable({
|
||||
greedy: true,
|
||||
tolerance: 'pointer',
|
||||
accept: function(el) {
|
||||
return ($(el).data('type') === 'media' && $(el).data('subType') === 'image');
|
||||
},
|
||||
drop: _.debounce(function(event, ui) {
|
||||
var $draggable = $(ui.draggable[0]);
|
||||
bgImageFileName.html($draggable.data('title'));
|
||||
mediaName = $draggable.data('cardTitle');
|
||||
|
||||
if($draggable.hasClass('from-provider')) {
|
||||
backgroundImageHandleDrop($draggable.data('providerData'), true);
|
||||
} else {
|
||||
backgroundImageHandleDrop($draggable.data('mediaId'));
|
||||
}
|
||||
}, 200)
|
||||
});
|
||||
|
||||
$('.background-image-drop').on('click', function() {
|
||||
var selectedCard = lD.toolbar.selectedCard;
|
||||
var fromProvider = selectedCard.hasClass('from-provider');
|
||||
var cardData = (fromProvider) ? selectedCard.data('providerData') : selectedCard.data('mediaId');
|
||||
|
||||
bgImageFileName.html(selectedCard.data('cardTitle'));
|
||||
mediaName = selectedCard.data('cardTitle');
|
||||
|
||||
backgroundImageHandleDrop(cardData, fromProvider);
|
||||
});
|
||||
|
||||
$("#layoutEditForm").submit(function(e) {
|
||||
e.preventDefault();
|
||||
var form = $(this);
|
||||
|
||||
$.ajax({
|
||||
type: form.attr("method"),
|
||||
url: form.attr("action"),
|
||||
cache: false,
|
||||
dataType: "json",
|
||||
data: $(form).serialize(),
|
||||
success: function(xhr, textStatus, error) {
|
||||
XiboSubmitResponse(xhr, form);
|
||||
|
||||
if (xhr.success) {
|
||||
var layout = $("div#layout");
|
||||
|
||||
if (layout.length > 0) {
|
||||
var color = form.find("#backgroundColor").val();
|
||||
layout.data().backgroundColor = color;
|
||||
layout.css("background-color", color);
|
||||
|
||||
if (backgroundChanged)
|
||||
window.location.reload();
|
||||
} else {
|
||||
if (backgroundChanged && typeof(table) !== 'undefined' && table.hasOwnProperty('ajax'))
|
||||
table.ajax.reload(null, false);
|
||||
}
|
||||
}
|
||||
},
|
||||
error: function(xhr, textStatus, errorThrown) {
|
||||
SystemMessage(xhr.responseText, false);
|
||||
}
|
||||
});
|
||||
})
|
||||
};
|
||||
|
||||
/**
|
||||
* Layout edit background add image button
|
||||
*/
|
||||
function layoutEditBackgroundButtonClicked(e, dialog) {
|
||||
e.preventDefault();
|
||||
|
||||
openUploadForm({
|
||||
url: $(e.target).data().addNewBackgroundUrl,
|
||||
title: "{% trans "Add Background Image" %}",
|
||||
videoImageCovers: false,
|
||||
buttons: {
|
||||
main: {
|
||||
label: "{% trans "Done" %}",
|
||||
className: "btn-primary btn-bb-main",
|
||||
callback: function () {
|
||||
XiboDialogClose();
|
||||
}
|
||||
}
|
||||
},
|
||||
templateOptions: {
|
||||
multi: false,
|
||||
trans: {
|
||||
addFiles: "{% trans "Browse/Add Image" %}",
|
||||
startUpload: "{% trans "Start Upload" %}",
|
||||
cancelUpload: "{% trans "Cancel Upload" %}"
|
||||
},
|
||||
upload: {
|
||||
maxSize: {{ libraryUpload.maxSize }},
|
||||
maxSizeMessage: "{{ libraryUpload.maxSizeMessage }}",
|
||||
validExt: "{{ libraryUpload.validImageExt }}"
|
||||
}
|
||||
},
|
||||
uploadDoneEvent: function (data) {
|
||||
var mediaId = data.result.files[0].mediaId;
|
||||
|
||||
if ($(dialog).find('[name="backgroundImageId"]').length === 0) {
|
||||
$('<input>').attr({
|
||||
type: 'hidden',
|
||||
name: 'backgroundImageId',
|
||||
value: mediaId
|
||||
}).appendTo(dialog);
|
||||
} else {
|
||||
$('[name="backgroundImageId"]').val(mediaId);
|
||||
}
|
||||
|
||||
dialog.find("#bg_not_found_icon").hide();
|
||||
dialog.find("#backgroundRemoveButton").removeClass("disabled");
|
||||
|
||||
XiboDialogClose();
|
||||
$('[name="backgroundImageId"]').trigger('change');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function layoutPublishFormOpen() {
|
||||
}
|
||||
|
||||
function layoutEditFormSaved() {
|
||||
lD.reloadData(lD.layout, {
|
||||
refreshEditor: true,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{# ── Embed mode: postMessage bridge ──────────────────────── #}
|
||||
<script type="text/javascript" nonce="{{ cspNonce }}">
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
var isEmbed = false;
|
||||
|
||||
try {
|
||||
isEmbed = params.get('embed') === '1' || window !== window.parent;
|
||||
} catch(e) {
|
||||
isEmbed = true;
|
||||
}
|
||||
|
||||
if (!isEmbed) return;
|
||||
|
||||
// Add embed class to body once DOM is ready
|
||||
document.body.classList.add('ots-embed-mode');
|
||||
|
||||
var layoutId = document.getElementById('layout-editor')
|
||||
? document.getElementById('layout-editor').getAttribute('data-layout-id')
|
||||
: null;
|
||||
|
||||
// TODO: For production, restrict targetOrigin to your app's domain
|
||||
var targetOrigin = '*';
|
||||
|
||||
/**
|
||||
* Send a message to the parent window.
|
||||
*/
|
||||
function sendToParent(type, data) {
|
||||
if (window.parent && window.parent !== window) {
|
||||
var message = { type: type, layoutId: layoutId };
|
||||
if (data) {
|
||||
for (var key in data) {
|
||||
if (data.hasOwnProperty(key)) {
|
||||
message[key] = data[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
window.parent.postMessage(message, targetOrigin);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Intercept editor save/publish via ajaxComplete ──────
|
||||
$(document).ajaxComplete(function(event, xhr, settings) {
|
||||
if (!settings || !settings.url) return;
|
||||
|
||||
try {
|
||||
var response = xhr.responseJSON || {};
|
||||
if (!response.success) return;
|
||||
|
||||
// Detect layout save (PUT to layout endpoint)
|
||||
if (settings.type === 'PUT' && settings.url.match(/\/layout\/\d+/)) {
|
||||
sendToParent('xibo:editor:save', { url: settings.url });
|
||||
}
|
||||
|
||||
// Detect layout publish
|
||||
if (settings.url.match(/\/layout\/publish\/\d+/)) {
|
||||
sendToParent('xibo:editor:publish', { url: settings.url });
|
||||
}
|
||||
|
||||
// Detect checkout
|
||||
if (settings.url.match(/\/layout\/checkout\/\d+/)) {
|
||||
sendToParent('xibo:editor:checkout', {
|
||||
url: settings.url,
|
||||
newLayoutId: response.id || null
|
||||
});
|
||||
}
|
||||
} catch(e) {
|
||||
// Silently ignore parse errors
|
||||
}
|
||||
});
|
||||
|
||||
// ── Intercept Back/Exit navigation ──────────────────────
|
||||
// The editor sets window.location.href to exitURL — intercept it
|
||||
$(document).on('click', '.back-button a, .editor-close-btn', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
sendToParent('xibo:editor:exit', {});
|
||||
});
|
||||
|
||||
// ── Listen for commands from parent ─────────────────────
|
||||
window.addEventListener('message', function(event) {
|
||||
var msg = event.data;
|
||||
if (!msg || typeof msg.type !== 'string') return;
|
||||
|
||||
switch (msg.type) {
|
||||
case 'xibo:editor:requestSave':
|
||||
// Trigger the properties panel save if pending
|
||||
if (typeof lD !== 'undefined' && lD.propertiesPanel && lD.propertiesPanel.toSave) {
|
||||
lD.propertiesPanel.save({ target: lD.selectedObject });
|
||||
}
|
||||
break;
|
||||
|
||||
case 'xibo:editor:requestPublish':
|
||||
if (typeof lD !== 'undefined' && lD.showPublishScreen) {
|
||||
lD.showPublishScreen();
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// ── Notify parent when editor is ready ──────────────────
|
||||
// Wait for the layout editor to initialize (it adds .editor-opened to body)
|
||||
var readyObserver = new MutationObserver(function(mutations) {
|
||||
if (document.body.classList.contains('editor-opened')) {
|
||||
sendToParent('xibo:editor:ready', {});
|
||||
readyObserver.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
if (document.body.classList.contains('editor-opened')) {
|
||||
// Already ready (unlikely but safe)
|
||||
sendToParent('xibo:editor:ready', {});
|
||||
} else {
|
||||
readyObserver.observe(document.body, { attributes: true, attributeFilter: ['class'] });
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user