Files
jmiller 7fb7e38762
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 6s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Secret Scan (pull_request) Successful in 10s
Universal: Auto Version Bump / Version Bump (push) Successful in 14s
Generic: Project CI / Lint & Validate (pull_request) Successful in 16s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Failing after 14s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 16s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 47s
Generic: Project CI / Tests (pull_request) Has been cancelled
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Has been cancelled
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Has been cancelled
Joomla: Extension CI / PHPStan Analysis (pull_request) Has been cancelled
Joomla: Extension CI / Build RC Pre-Release (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report: Scripts Governance (pull_request) Has been cancelled
Generic: Repo Health / Report: Repository Health (pull_request) Has been cancelled
refactor: rename MokoJoomOpenGraph -> MokoSuiteOpenGraph; require Joomla 6+
Product rename (display name / docs / comments / language strings only —
technical element names mokoog/com_mokoog/MokoOG namespace unchanged):
- Replace "MokoJoom" -> "MokoSuite" across 55 files
- Fixes the update-site license lookup in script.php, which matched the
  old "%MokoJoomOpenGraph%" name and would never find a "MokoSuite" site

Joomla 6 compatibility:
- script.php: minimumJoomla 4.0.0 -> 6.0.0, minimumPhp 8.1.0 -> 8.2.0,
  and actually enforce the Joomla floor in preflight() (was PHP-only)
- Add PKG_MOKOOG_JOOMLA_VERSION_ERROR language strings (en-GB, en-US)
- openapi.yaml + README state Joomla 6.0+ requirement
- Audit confirmed the codebase already uses only Joomla-6-supported APIs
2026-06-28 14:33:35 -05:00

500 lines
18 KiB
JavaScript

/**
* @package MokoSuiteOpenGraph
* @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'),
seoTitle: document.getElementById('jform_mokoog_seo_title'),
metaDescription: document.getElementById('jform_mokoog_meta_description')
};
// Character count indicators
var charLimits = [
{ field: fields.ogTitle, optimal: 60, max: 90 },
{ field: fields.ogDesc, optimal: 155, max: 200 },
{ field: fields.seoTitle, optimal: 60, max: 70 },
{ field: fields.metaDescription, optimal: 155, max: 160 }
];
charLimits.forEach(function (cfg) {
if (!cfg.field) return;
var counter = document.createElement('span');
counter.className = 'mokoog-char-count';
cfg.field.parentNode.appendChild(counter);
function refresh() {
var len = cfg.field.value.length;
counter.textContent = len + '/' + cfg.optimal;
if (len > cfg.max) {
counter.className = 'mokoog-char-count mokoog-char-over';
} else if (len > cfg.optimal) {
counter.className = 'mokoog-char-count mokoog-char-warn';
} else {
counter.className = 'mokoog-char-count mokoog-char-ok';
}
}
cfg.field.addEventListener('input', refresh);
cfg.field.addEventListener('change', refresh);
refresh();
});
// AI Generate buttons
['ogTitle', 'ogDesc'].forEach(function(fieldKey) {
var field = fields[fieldKey];
if (!field) return;
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'btn btn-sm btn-outline-primary mokoog-ai-btn';
btn.textContent = 'Generate with AI';
btn.dataset.target = fieldKey;
field.parentNode.appendChild(btn);
btn.addEventListener('click', function() {
var articleTitle = fields.articleTitle ? fields.articleTitle.value : '';
btn.disabled = true;
btn.textContent = 'Generating...';
var formData = new FormData();
formData.append('task', 'mokoog.aiGenerate');
formData.append('field', fieldKey === 'ogTitle' ? 'title' : 'description');
formData.append('article_title', articleTitle);
formData.append(Joomla.getOptions('csrf.token'), 1);
fetch(window.location.origin + '/administrator/index.php?option=com_ajax&plugin=mokoog&group=system&format=json', {
method: 'POST',
body: formData
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.data && data.data[0]) {
field.value = data.data[0];
field.dispatchEvent(new Event('input'));
}
btn.disabled = false;
btn.textContent = 'Generate with AI';
})
.catch(function() {
btn.disabled = false;
btn.textContent = 'Generate with AI';
});
});
});
// 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);
// LinkedIn preview card
var liLabel = document.createElement('small');
liLabel.className = 'mokoog-platform-label';
liLabel.textContent = 'LinkedIn';
wrapper.appendChild(liLabel);
var liCard = document.createElement('div');
liCard.className = 'mokoog-card mokoog-card-li';
var liImg = document.createElement('div');
liImg.id = 'mokoog-li-img';
liImg.className = 'mokoog-card-img';
liCard.appendChild(liImg);
var liBody = document.createElement('div');
liBody.className = 'mokoog-card-body';
var liTitle = document.createElement('div');
liTitle.id = 'mokoog-li-title';
liTitle.className = 'mokoog-card-title';
liBody.appendChild(liTitle);
var liDomain = document.createElement('div');
liDomain.id = 'mokoog-li-domain';
liDomain.className = 'mokoog-card-domain';
liBody.appendChild(liDomain);
liCard.appendChild(liBody);
wrapper.appendChild(liCard);
// Discord preview card
var dcLabel = document.createElement('small');
dcLabel.className = 'mokoog-platform-label';
dcLabel.textContent = 'Discord';
wrapper.appendChild(dcLabel);
var dcCard = document.createElement('div');
dcCard.className = 'mokoog-card mokoog-card-dc';
var dcBody = document.createElement('div');
dcBody.className = 'mokoog-card-body';
var dcTitle = document.createElement('div');
dcTitle.id = 'mokoog-dc-title';
dcTitle.className = 'mokoog-card-title';
dcBody.appendChild(dcTitle);
var dcDesc = document.createElement('div');
dcDesc.id = 'mokoog-dc-desc';
dcDesc.className = 'mokoog-card-desc';
dcBody.appendChild(dcDesc);
var dcDomain = document.createElement('div');
dcDomain.id = 'mokoog-dc-domain';
dcDomain.className = 'mokoog-card-domain';
dcBody.appendChild(dcDomain);
dcCard.appendChild(dcBody);
var dcImg = document.createElement('div');
dcImg.id = 'mokoog-dc-img';
dcImg.className = 'mokoog-card-img';
dcCard.appendChild(dcImg);
wrapper.appendChild(dcCard);
// Mastodon preview card
var maLabel = document.createElement('small');
maLabel.className = 'mokoog-platform-label';
maLabel.textContent = 'Mastodon';
wrapper.appendChild(maLabel);
var maCard = document.createElement('div');
maCard.className = 'mokoog-card mokoog-card-ma';
var maImg = document.createElement('div');
maImg.id = 'mokoog-ma-img';
maImg.className = 'mokoog-card-img';
maCard.appendChild(maImg);
var maBody = document.createElement('div');
maBody.className = 'mokoog-card-body';
var maTitle = document.createElement('div');
maTitle.id = 'mokoog-ma-title';
maTitle.className = 'mokoog-card-title';
maBody.appendChild(maTitle);
var maDesc = document.createElement('div');
maDesc.id = 'mokoog-ma-desc';
maDesc.className = 'mokoog-card-desc';
maBody.appendChild(maDesc);
var maDomain = document.createElement('div');
maDomain.id = 'mokoog-ma-domain';
maDomain.className = 'mokoog-card-domain';
maBody.appendChild(maDomain);
maCard.appendChild(maBody);
wrapper.appendChild(maCard);
// Slack preview card
var slLabel = document.createElement('small');
slLabel.className = 'mokoog-platform-label';
slLabel.textContent = 'Slack';
wrapper.appendChild(slLabel);
var slCard = document.createElement('div');
slCard.className = 'mokoog-card mokoog-card-sl';
var slBody = document.createElement('div');
slBody.className = 'mokoog-card-body';
var slTitle = document.createElement('div');
slTitle.id = 'mokoog-sl-title';
slTitle.className = 'mokoog-card-title';
slBody.appendChild(slTitle);
var slDesc = document.createElement('div');
slDesc.id = 'mokoog-sl-desc';
slDesc.className = 'mokoog-card-desc';
slBody.appendChild(slDesc);
var slDomain = document.createElement('div');
slDomain.id = 'mokoog-sl-domain';
slDomain.className = 'mokoog-card-domain';
slBody.appendChild(slDomain);
slCard.appendChild(slBody);
wrapper.appendChild(slCard);
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';
}
// LinkedIn (shorter truncation: title 70, no description shown in card)
var liTitle = title.length > 70 ? title.substring(0, 67) + '...' : title;
document.getElementById('mokoog-li-title').textContent = liTitle;
document.getElementById('mokoog-li-domain').textContent = domain;
var liImgEl = document.getElementById('mokoog-li-img');
if (img) {
liImgEl.style.backgroundImage = 'url(' + encodeURI(img) + ')';
liImgEl.style.display = '';
} else {
liImgEl.style.display = 'none';
}
// Discord (title 256, desc 350)
var dcTitle = title.length > 256 ? title.substring(0, 253) + '...' : title;
var dcDesc = desc.length > 350 ? desc.substring(0, 347) + '...' : desc;
document.getElementById('mokoog-dc-title').textContent = dcTitle;
document.getElementById('mokoog-dc-desc').textContent = dcDesc;
document.getElementById('mokoog-dc-domain').textContent = domain;
var dcImgEl = document.getElementById('mokoog-dc-img');
if (img) {
dcImgEl.style.backgroundImage = 'url(' + encodeURI(img) + ')';
dcImgEl.style.display = '';
} else {
dcImgEl.style.display = 'none';
}
// Mastodon (title 70, desc 200)
var maTitle = title.length > 70 ? title.substring(0, 67) + '...' : title;
var maDesc = desc.length > 200 ? desc.substring(0, 197) + '...' : desc;
document.getElementById('mokoog-ma-title').textContent = maTitle;
document.getElementById('mokoog-ma-desc').textContent = maDesc;
document.getElementById('mokoog-ma-domain').textContent = domain;
var maImgEl = document.getElementById('mokoog-ma-img');
if (img) {
maImgEl.style.backgroundImage = 'url(' + encodeURI(img) + ')';
maImgEl.style.display = '';
} else {
maImgEl.style.display = 'none';
}
// Slack (title 70, desc 150, no image)
var slTitle = title.length > 70 ? title.substring(0, 67) + '...' : title;
var slDesc = desc.length > 150 ? desc.substring(0, 147) + '...' : desc;
document.getElementById('mokoog-sl-title').textContent = slTitle;
document.getElementById('mokoog-sl-desc').textContent = slDesc;
document.getElementById('mokoog-sl-domain').textContent = domain;
}
// SEO scoring panel
var seoChecks = [
{ id: 'og-title', label: 'OG Title', check: function() { return fields.ogTitle && fields.ogTitle.value.length > 0; }},
{ id: 'og-desc', label: 'OG Description', check: function() { return fields.ogDesc && fields.ogDesc.value.length > 0; }},
{ id: 'og-image', label: 'OG Image', check: function() { return fields.ogImage && fields.ogImage.value.length > 0; }},
{ id: 'seo-title', label: 'SEO Title', check: function() { return fields.seoTitle && fields.seoTitle.value.length > 0; }},
{ id: 'meta-desc', label: 'Meta Description', check: function() { return fields.metaDescription && fields.metaDescription.value.length > 0; }},
{ id: 'title-length', label: 'Title Length (\u226460)', check: function() {
var t = (fields.ogTitle && fields.ogTitle.value) || (fields.articleTitle && fields.articleTitle.value) || '';
return t.length > 0 && t.length <= 60;
}},
{ id: 'desc-length', label: 'Description Length (\u2264160)', check: function() {
var d = (fields.ogDesc && fields.ogDesc.value) || (fields.metaDesc && fields.metaDesc.value) || '';
return d.length > 0 && d.length <= 160;
}}
];
var seoPanel = document.createElement('div');
seoPanel.className = 'mokoog-seo-score';
var seoHeading = document.createElement('h4');
seoHeading.className = 'mokoog-seo-heading';
seoHeading.textContent = 'SEO Analysis';
seoPanel.appendChild(seoHeading);
var seoList = document.createElement('ul');
seoList.className = 'mokoog-seo-list';
var seoDots = {};
seoChecks.forEach(function (chk) {
var li = document.createElement('li');
li.className = 'mokoog-seo-item';
var dot = document.createElement('span');
dot.className = 'mokoog-seo-dot mokoog-seo-fail';
seoDots[chk.id] = dot;
li.appendChild(dot);
var label = document.createElement('span');
label.textContent = chk.label;
li.appendChild(label);
seoList.appendChild(li);
});
seoPanel.appendChild(seoList);
var seoTotal = document.createElement('div');
seoTotal.className = 'mokoog-seo-total';
seoPanel.appendChild(seoTotal);
wrapper.parentNode.insertBefore(seoPanel, wrapper.nextSibling);
function updateSeoScore() {
var passed = 0;
seoChecks.forEach(function (chk) {
var ok = chk.check();
if (ok) passed++;
seoDots[chk.id].className = 'mokoog-seo-dot ' + (ok ? 'mokoog-seo-pass' : 'mokoog-seo-fail');
});
seoTotal.textContent = passed + '/' + seoChecks.length + ' checks passed';
if (passed === seoChecks.length) {
seoTotal.className = 'mokoog-seo-total mokoog-seo-total-good';
} else if (passed >= Math.ceil(seoChecks.length / 2)) {
seoTotal.className = 'mokoog-seo-total mokoog-seo-total-ok';
} else {
seoTotal.className = 'mokoog-seo-total mokoog-seo-total-bad';
}
}
Object.values(fields).forEach(function (el) {
if (el) {
el.addEventListener('input', function () { updatePreview(); updateSeoScore(); });
el.addEventListener('change', function () { updatePreview(); updateSeoScore(); });
}
});
if (fields.ogImage) {
var observer = new MutationObserver(function () { updatePreview(); updateSeoScore(); });
observer.observe(fields.ogImage, { attributes: true, attributeFilter: ['value'] });
}
updatePreview();
updateSeoScore();
});