feat(preview): live social sharing preview in article editor (closes #3)
- Facebook and Twitter/X card previews update in real-time - Renders below the OG fieldset in article/menu editors - Reads og_title, og_description, og_image with fallback to article title and meta description - CSS mockups of each platform's card layout - Registered via joomla.asset.json Web Asset Manager - Safe DOM construction (no innerHTML with user data) - MutationObserver watches media field for image changes Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* @package MokoOpenGraph
|
||||
* @subpackage plg_content_mokoog
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
.mokoog-preview-wrapper {
|
||||
margin: 15px 0;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.mokoog-preview-heading {
|
||||
margin: 0 0 12px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.mokoog-platform-label {
|
||||
display: block;
|
||||
color: #999;
|
||||
text-transform: uppercase;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.mokoog-platform-label:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.mokoog-card {
|
||||
overflow: hidden;
|
||||
max-width: 500px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.mokoog-card-fb {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.mokoog-card-tw {
|
||||
border: 1px solid #cfd9de;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.mokoog-card-img {
|
||||
height: 260px;
|
||||
background: #e4e6eb center / cover no-repeat;
|
||||
}
|
||||
|
||||
.mokoog-card-body {
|
||||
padding: 10px 12px;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.mokoog-card-tw .mokoog-card-body {
|
||||
border-top-color: #cfd9de;
|
||||
}
|
||||
|
||||
.mokoog-card-domain {
|
||||
font-size: 11px;
|
||||
color: #65676b;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.mokoog-card-tw .mokoog-card-domain {
|
||||
font-size: 13px;
|
||||
text-transform: none;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.mokoog-card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1d2129;
|
||||
margin: 3px 0 2px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.mokoog-card-tw .mokoog-card-title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #0f1419;
|
||||
}
|
||||
|
||||
.mokoog-card-desc {
|
||||
font-size: 14px;
|
||||
color: #65676b;
|
||||
line-height: 1.4;
|
||||
max-height: 2.8em;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mokoog-card-tw .mokoog-card-desc {
|
||||
font-size: 15px;
|
||||
color: #536471;
|
||||
margin-top: 2px;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"$schema": "https://developer.joomla.org/schemas/json-schema/web_assets.json",
|
||||
"name": "plg_content_mokoog",
|
||||
"version": "01.00.00",
|
||||
"description": "MokoOpenGraph Content Plugin Assets",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"assets": [
|
||||
{
|
||||
"name": "plg_content_mokoog.preview",
|
||||
"type": "style",
|
||||
"uri": "plg_content_mokoog/css/preview.css"
|
||||
},
|
||||
{
|
||||
"name": "plg_content_mokoog.preview",
|
||||
"type": "script",
|
||||
"uri": "plg_content_mokoog/js/preview.js",
|
||||
"dependencies": ["core"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* @package MokoOpenGraph
|
||||
* @subpackage plg_content_mokoog
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GPL-3.0-or-later
|
||||
*
|
||||
* Live social sharing preview for article/menu item editor.
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
'use strict';
|
||||
|
||||
var fields = {
|
||||
ogTitle: document.getElementById('jform_mokoog_og_title'),
|
||||
ogDesc: document.getElementById('jform_mokoog_og_description'),
|
||||
ogImage: document.getElementById('jform_mokoog_og_image'),
|
||||
articleTitle: document.getElementById('jform_title'),
|
||||
metaDesc: document.getElementById('jform_metadesc')
|
||||
};
|
||||
|
||||
// Find the mokoog fieldset and insert preview after it
|
||||
var fieldset = document.querySelector('[data-showon-id="mokoog"]') ||
|
||||
document.getElementById('attrib-mokoog') ||
|
||||
document.querySelector('fieldset.mokoog') ||
|
||||
document.querySelector('[id*="mokoog"]');
|
||||
|
||||
if (!fieldset) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Build preview DOM safely (no innerHTML with user data)
|
||||
var preview = document.createElement('div');
|
||||
preview.id = 'mokoog-preview';
|
||||
|
||||
var wrapper = document.createElement('div');
|
||||
wrapper.className = 'mokoog-preview-wrapper';
|
||||
|
||||
var heading = document.createElement('h4');
|
||||
heading.className = 'mokoog-preview-heading';
|
||||
heading.textContent = 'Social Sharing Preview';
|
||||
wrapper.appendChild(heading);
|
||||
|
||||
// Facebook preview card
|
||||
var fbLabel = document.createElement('small');
|
||||
fbLabel.className = 'mokoog-platform-label';
|
||||
fbLabel.textContent = 'Facebook';
|
||||
wrapper.appendChild(fbLabel);
|
||||
|
||||
var fbCard = document.createElement('div');
|
||||
fbCard.className = 'mokoog-card mokoog-card-fb';
|
||||
|
||||
var fbImg = document.createElement('div');
|
||||
fbImg.id = 'mokoog-fb-img';
|
||||
fbImg.className = 'mokoog-card-img';
|
||||
fbCard.appendChild(fbImg);
|
||||
|
||||
var fbBody = document.createElement('div');
|
||||
fbBody.className = 'mokoog-card-body';
|
||||
|
||||
var fbDomain = document.createElement('div');
|
||||
fbDomain.id = 'mokoog-fb-domain';
|
||||
fbDomain.className = 'mokoog-card-domain';
|
||||
fbBody.appendChild(fbDomain);
|
||||
|
||||
var fbTitle = document.createElement('div');
|
||||
fbTitle.id = 'mokoog-fb-title';
|
||||
fbTitle.className = 'mokoog-card-title';
|
||||
fbBody.appendChild(fbTitle);
|
||||
|
||||
var fbDesc = document.createElement('div');
|
||||
fbDesc.id = 'mokoog-fb-desc';
|
||||
fbDesc.className = 'mokoog-card-desc';
|
||||
fbBody.appendChild(fbDesc);
|
||||
|
||||
fbCard.appendChild(fbBody);
|
||||
wrapper.appendChild(fbCard);
|
||||
|
||||
// Twitter preview card
|
||||
var twLabel = document.createElement('small');
|
||||
twLabel.className = 'mokoog-platform-label';
|
||||
twLabel.textContent = 'Twitter / X';
|
||||
wrapper.appendChild(twLabel);
|
||||
|
||||
var twCard = document.createElement('div');
|
||||
twCard.className = 'mokoog-card mokoog-card-tw';
|
||||
|
||||
var twImg = document.createElement('div');
|
||||
twImg.id = 'mokoog-tw-img';
|
||||
twImg.className = 'mokoog-card-img';
|
||||
twCard.appendChild(twImg);
|
||||
|
||||
var twBody = document.createElement('div');
|
||||
twBody.className = 'mokoog-card-body';
|
||||
|
||||
var twTitle = document.createElement('div');
|
||||
twTitle.id = 'mokoog-tw-title';
|
||||
twTitle.className = 'mokoog-card-title';
|
||||
twBody.appendChild(twTitle);
|
||||
|
||||
var twDesc = document.createElement('div');
|
||||
twDesc.id = 'mokoog-tw-desc';
|
||||
twDesc.className = 'mokoog-card-desc';
|
||||
twBody.appendChild(twDesc);
|
||||
|
||||
var twDomain = document.createElement('div');
|
||||
twDomain.id = 'mokoog-tw-domain';
|
||||
twDomain.className = 'mokoog-card-domain';
|
||||
twBody.appendChild(twDomain);
|
||||
|
||||
twCard.appendChild(twBody);
|
||||
wrapper.appendChild(twCard);
|
||||
|
||||
preview.appendChild(wrapper);
|
||||
fieldset.parentNode.insertBefore(preview, fieldset.nextSibling);
|
||||
|
||||
var domain = window.location.hostname;
|
||||
|
||||
function updatePreview() {
|
||||
var title = (fields.ogTitle && fields.ogTitle.value) ||
|
||||
(fields.articleTitle && fields.articleTitle.value) || 'Page Title';
|
||||
var desc = (fields.ogDesc && fields.ogDesc.value) ||
|
||||
(fields.metaDesc && fields.metaDesc.value) || 'Page description will appear here...';
|
||||
var img = '';
|
||||
|
||||
if (fields.ogImage) {
|
||||
img = fields.ogImage.value;
|
||||
}
|
||||
|
||||
if (title.length > 65) title = title.substring(0, 62) + '...';
|
||||
if (desc.length > 160) desc = desc.substring(0, 157) + '...';
|
||||
|
||||
// Facebook
|
||||
document.getElementById('mokoog-fb-title').textContent = title;
|
||||
document.getElementById('mokoog-fb-desc').textContent = desc;
|
||||
document.getElementById('mokoog-fb-domain').textContent = domain;
|
||||
var fbImgEl = document.getElementById('mokoog-fb-img');
|
||||
if (img) {
|
||||
fbImgEl.style.backgroundImage = 'url(' + encodeURI(img) + ')';
|
||||
fbImgEl.style.display = '';
|
||||
} else {
|
||||
fbImgEl.style.display = 'none';
|
||||
}
|
||||
|
||||
// Twitter
|
||||
document.getElementById('mokoog-tw-title').textContent = title;
|
||||
document.getElementById('mokoog-tw-desc').textContent = desc;
|
||||
document.getElementById('mokoog-tw-domain').textContent = domain;
|
||||
var twImgEl = document.getElementById('mokoog-tw-img');
|
||||
if (img) {
|
||||
twImgEl.style.backgroundImage = 'url(' + encodeURI(img) + ')';
|
||||
twImgEl.style.display = '';
|
||||
} else {
|
||||
twImgEl.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
Object.values(fields).forEach(function (el) {
|
||||
if (el) {
|
||||
el.addEventListener('input', updatePreview);
|
||||
el.addEventListener('change', updatePreview);
|
||||
}
|
||||
});
|
||||
|
||||
if (fields.ogImage) {
|
||||
var observer = new MutationObserver(updatePreview);
|
||||
observer.observe(fields.ogImage, { attributes: true, attributeFilter: ['value'] });
|
||||
}
|
||||
|
||||
updatePreview();
|
||||
});
|
||||
@@ -27,6 +27,12 @@
|
||||
<folder>language</folder>
|
||||
</files>
|
||||
|
||||
<media destination="plg_content_mokoog" folder="media">
|
||||
<filename>joomla.asset.json</filename>
|
||||
<folder>js</folder>
|
||||
<folder>css</folder>
|
||||
</media>
|
||||
|
||||
<languages>
|
||||
<language tag="en-GB">language/en-GB/plg_content_mokoog.ini</language>
|
||||
<language tag="en-GB">language/en-GB/plg_content_mokoog.sys.ini</language>
|
||||
|
||||
@@ -72,6 +72,12 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface
|
||||
Form::addFormPath($formPath);
|
||||
$form->loadFile('mokoog', false);
|
||||
|
||||
// Load live preview assets
|
||||
$wa = $this->getApplication()->getDocument()->getWebAssetManager();
|
||||
$wa->getRegistry()->addRegistryFile('media/plg_content_mokoog/joomla.asset.json');
|
||||
$wa->useStyle('plg_content_mokoog.preview');
|
||||
$wa->useScript('plg_content_mokoog.preview');
|
||||
|
||||
// If editing an existing item, load saved OG data
|
||||
$id = 0;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user