Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions _bootstrap/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,13 @@
], ['pluginid','event'], false)) {
echo "Error creating modPluginEvent.\n";
}
if (!createObject('modPluginEvent', [
'pluginid' => $vcPlugin->get('id'),
'event' => 'ContentBlocks_AfterFieldRender',
'priority' => 0,
], ['pluginid','event'], false)) {
echo "Error creating modPluginEvent.\n";
}
}


Expand Down
1 change: 1 addition & 0 deletions _build/events/events.magicpreview.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
'OnDocFormSave',
'OnLoadWebDocument',
'OnManagerPageBeforeRender',
'ContentBlocks_AfterFieldRender',
];

foreach ($e as $ev) {
Expand Down
58 changes: 56 additions & 2 deletions assets/components/magicpreview/css/mgr.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@
========================================================================== */

/* --------------------------------------------------------------------------
Draft banner: notification bar above the resource panel.
Appended to #modx-panel-resource-div via plain DOM.
Draft banner: fixed notification bar at the top of the viewport.
Appended to document.body so it sits outside the ExtJS layout and
does not affect resource-panel height calculations.
-------------------------------------------------------------------------- */

.mmmp-draft-banner {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 200;
display: flex;
align-items: center;
gap: 0.75rem;
Expand Down Expand Up @@ -84,6 +90,43 @@
background: #E4E4E4;
}

/* In MODX 2 the manager header is ~55px tall; place the banner below it */
body.magicpreview_modx2 .mmmp-draft-banner {
top: 55px;
}

/* Push the ExtJS layout panels below the banner.
CSS handles the top-position shift; relayoutModx() overrides getViewSize()
to return a reduced height so the border layout recalculates every inner
panel-body height — ensuring nothing is clipped at the bottom.
MODX 3 desktop: #modx-header = west icon bar; #modx-split-wrapper = center
(the nested border layout that contains the tree + #modx-content). */
@media (min-width: 641px) {
body.magicpreview_modx3:has(.mmmp-draft-banner) #modx-header,
body.magicpreview_modx3:has(.mmmp-draft-banner) #modx-split-wrapper,
body.magicpreview_modx3:has(.mmmp-draft-banner) #modx-content {
top: 45px !important;
height: calc(100vh - 45px) !important;
}
}

/* MODX 2: north region stays at 0–55px; banner sits at top: 55px.
Shift the west and center panels below the banner (55 + 45 = 100px). */
body.magicpreview_modx2:has(.mmmp-draft-banner) #modx-leftbar,
body.magicpreview_modx2:has(.mmmp-draft-banner) #modx-content {
top: 100px !important;
height: calc(100vh - 100px) !important;
}

/* Push the preview panel below the banner */
body:has(.mmmp-draft-banner) .mmmp-panel {
top: 45px;
}

body.magicpreview_modx2:has(.mmmp-draft-banner) .mmmp-panel {
top: 100px;
}

/* Push the fixed-position action buttons below the banner */
body:has(.mmmp-draft-banner) #modx-action-buttons {
top: 48px;
Expand Down Expand Up @@ -519,3 +562,14 @@ a.mmmp-share-revoke:hover,
a.mmmp-share-revoke:focus {
color: #843534;
}

/* --------------------------------------------------------------------------
Click-to-field: brief highlight on the manager field scrolled to from the
preview. Transition fades the outline out after the initial flash.
-------------------------------------------------------------------------- */

.mmmp-field-highlight {
outline: 2px solid hsl(207, 70%, 53%);
outline-offset: 3px;
transition: outline-color 1.2s ease-out;
}
52 changes: 34 additions & 18 deletions assets/components/magicpreview/js/panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -514,15 +514,15 @@
var _originalGetViewSize = null;

/**
* Overrides the ExtJS Viewport's size calculation so its border
* layout positions panels within a narrower area, leaving room
* for our preview panel on the right.
* Overrides the ExtJS Viewport's getViewSize() so the border layout
* recalculates within the available space, then calls doLayout() to
* cascade setSize()/onResize() through every child panel.
*
* Ext.Viewport always measures document.body, and its getViewSize()
* returns window.innerWidth regardless of any CSS width constraints.
* We override that method on the Viewport's element so the border
* layout reads our reduced width, then call doLayout() to trigger
* a full recalculation.
* Width is reduced when the on-page preview panel is open (to leave
* room for it on the right). Height is reduced when the draft banner
* is visible (so the scroll container inside #modx-content is sized
* correctly and nothing is clipped behind the banner). Both reductions
* are applied together when both are active.
*/
function relayoutModx() {
var layout = Ext.getCmp('modx-layout');
Expand All @@ -531,18 +531,20 @@
}

var panelIsOpen = document.body.classList.contains('mmmp-panel-onpage-active');
var bannerEl = document.getElementById('mmmp-draft-banner');
var bannerH = bannerEl ? bannerEl.offsetHeight : 0;
var needsOverride = panelIsOpen || bannerH > 0;
var pw = panelIsOpen ? getPanelWidth() : 0;

if (panelIsOpen) {
if (needsOverride) {
// Store the original on first override
if (!_originalGetViewSize) {
_originalGetViewSize = layout.el.getViewSize.bind(layout.el);
}

var pw = getPanelWidth();
layout.el.getViewSize = function() {
return {
width: window.innerWidth - pw,
height: window.innerHeight
height: window.innerHeight - bannerH
};
};
} else if (_originalGetViewSize) {
Expand All @@ -553,15 +555,22 @@

// Delay to allow the panel to render and be measurable
setTimeout(function() {
// Re-read panel width now that it's in the DOM
if (panelIsOpen) {
var pw = getPanelWidth();
// Re-read both values now that the DOM has settled
var bannerElInner = document.getElementById('mmmp-draft-banner');
var bannerHInner = bannerElInner ? bannerElInner.offsetHeight : 0;
var panelIsOpenInner = document.body.classList.contains('mmmp-panel-onpage-active');
var pwInner = panelIsOpenInner ? getPanelWidth() : 0;
var needsOverrideInner = panelIsOpenInner || bannerHInner > 0;
if (needsOverrideInner) {
layout.el.getViewSize = function() {
return {
width: window.innerWidth - pw,
height: window.innerHeight
width: window.innerWidth - pwInner,
height: window.innerHeight - bannerHInner
};
};
} else if (_originalGetViewSize) {
delete layout.el.getViewSize;
_originalGetViewSize = null;
}
layout.doLayout();
}, 50);
Expand Down Expand Up @@ -651,6 +660,13 @@
* Set the last preview hash.
* @param {string|null} hash
*/
setLastHash: function(hash) { lastHash = hash; }
setLastHash: function(hash) { lastHash = hash; },

/**
* Re-run the ExtJS Viewport layout override. Called by preview.js
* when the draft banner is shown or hidden so the height reduction
* is applied consistently regardless of panel state.
*/
relayout: relayoutModx
};
})();
146 changes: 135 additions & 11 deletions assets/components/magicpreview/js/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,7 @@
var finish = function() {
if (banner) {
banner.remove();
MagicPreview._panel.relayout();
}
MODx.msg.status({
title: lexicon('draft_discarded'),
Expand Down Expand Up @@ -761,23 +762,18 @@
}

/**
* Shows a draft banner above the resource panel. Appended to the
* #modx-panel-resource-div container which sits directly above the
* ExtJS-rendered resource panel in the DOM. Offers View, Share,
* Restore and Discard for the saved draft; stays visible until the
* draft is restored or discarded.
* Shows a draft banner fixed at the top of the viewport. Appended to
* document.body so it sits entirely outside the ExtJS layout and does
* not affect the resource panel's height calculations. Offers View,
* Share, Restore and Discard for the saved draft; stays visible until
* the draft is restored or discarded.
*/
function showDraftBanner() {
var c = config();
if (!c.hasDraft) {
return;
}

var container = document.getElementById('modx-panel-resource-div');
if (!container) {
return;
}

var banner = document.createElement('div');
banner.id = 'mmmp-draft-banner';
banner.className = 'mmmp-draft-banner';
Expand All @@ -793,7 +789,8 @@
+ lexicon('draft_discard') + '</button>'
+ '</span>';

container.appendChild(banner);
document.body.appendChild(banner);
MagicPreview._panel.relayout();

// Delegate click events from the banner's buttons
banner.addEventListener('click', function(e) {
Expand Down Expand Up @@ -908,6 +905,105 @@
});
}

// =========================================================================
// Click-to-field: scroll the manager form to the ContentBlocks field
// matching a postMessage from the preview iframe. ContentBlocks is not
// assumed to be installed — the function is a no-op when no matching
// element exists, falling back to scrolling to page top.
// =========================================================================

/**
* Scroll the manager resource form to the ContentBlocks field identified
* by field/index, activating the correct tab first if needed.
*
* @param {string} field - Numeric ContentBlocks field id.
* @param {number} [index=0] - 0-based index when the same field type
* appears more than once on the page.
*/
function scrollToField(field, index) {
try {
var idx = typeof index === 'number' ? index : 0;
var el = null;

// data-field attribute — ContentBlocks manager <li data-field="5">
var byData = document.querySelectorAll('[data-field="' + CSS.escape(field) + '"]');
if (byData.length > idx) { el = byData[idx]; }

// Nothing matched — scroll to top so the user can orient themselves
if (!el) {
window.scrollTo({ top: 0, behavior: 'smooth' });
return;
}

// Activate the tab containing the element (Document, TV, Settings…) if it
// isn't already active. ExtJS keeps inactive tab content in the DOM (hidden
// via CSS), so dom.contains() finds elements regardless of active tab.
var needsTabSwitch = false;
try {
var tabPanel = Ext.getCmp('modx-resource-tabs');
if (tabPanel && tabPanel.items && tabPanel.getActiveTab) {
var activeTab = tabPanel.getActiveTab();
tabPanel.items.each(function(tab) {
try {
var tabEl = tab.getEl && tab.getEl();
if (tabEl && tabEl.dom && tabEl.dom.contains(el) && activeTab !== tab) {
tabPanel.setActiveTab(tab);
needsTabSwitch = true;
}
} catch (ex) {
console.error('[MagicPreview] scrollToField error:', ex);
}
});
}
} catch (ex) {
console.error('[MagicPreview] scrollToField error:', ex);
}

var doScroll = function() {
try {
// Scroll within the ExtJS center panel body
var scrollContainer = document.querySelector('#modx-content > .x-panel-body');
if (!scrollContainer) {
// Fallback: walk up the DOM and use the first real scroll container
var p = el.parentElement;
while (p && p !== document.documentElement) {
var st = window.getComputedStyle(p);
var ov = st.overflowY;
if ((ov === 'auto' || ov === 'scroll') && p.scrollHeight > p.clientHeight) {
scrollContainer = p;
break;
}
p = p.parentElement;
}
}
if (scrollContainer) {
var cRect = scrollContainer.getBoundingClientRect();
var eRect = el.getBoundingClientRect();
var desired = scrollContainer.scrollTop + (eRect.top - cRect.top) - (cRect.height - eRect.height) / 2;
scrollContainer.scrollTo({ top: Math.max(0, desired), behavior: 'smooth' });
}
if (!el.getAttribute('tabindex') && !/^(INPUT|TEXTAREA|SELECT|BUTTON|A)$/.test(el.tagName)) {
el.setAttribute('tabindex', '-1');
}
el.focus({ preventScroll: true });
el.classList.add('mmmp-field-highlight');
setTimeout(function() { el.classList.remove('mmmp-field-highlight'); }, 1200);
} catch (ex) {
console.error('[MagicPreview] scrollToField error:', ex);
}
};

// Brief delay after a tab switch so the ExtJS transition completes
if (needsTabSwitch) {
setTimeout(doScroll, 150);
} else {
doScroll();
}
} catch (ex) {
console.error('[MagicPreview] scrollToField error:', ex);
}
}

// =========================================================================
// ExtJS: Button injection
// =========================================================================
Expand Down Expand Up @@ -1145,5 +1241,33 @@
return;
}
}, true);

// =====================================================================
// Click-to-field: postMessage listener
// Accepts messages from the preview iframe (panel mode) or from the
// preview popup relayed via preview.tpl (window mode).
// =====================================================================

// Pre-compute once — origin never changes during the page session.
var previewOrigin = '';
try {
previewOrigin = new URL(config().baseFrameUrl).origin;
} catch (ex) {
console.error('[MagicPreview] scrollToField error:', ex);
}

window.addEventListener('message', function(e) {
var data = e.data;
if (!data || typeof data !== 'object' || data.type !== 'magicpreview:scrollToField') {
return;
}
// Accept from the frontend's origin (panel mode) or manager's own
// origin (preview.tpl relay for window mode).
if (e.origin !== previewOrigin && e.origin !== window.location.origin) {
return;
}
if (typeof data.field !== 'string' || !data.field) { return; }
scrollToField(data.field, data.index);
}, false);
});
})();
Loading