Skip to content
Merged
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
140 changes: 140 additions & 0 deletions assets/css/exelearning.css
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,106 @@
color: #fff;
}

/* Multi-format split download button */
.exelearning-block-toolbar {
display: flex;
justify-content: flex-end;
align-items: center;
padding: 6px 8px;
background: #f6f7f7;
border: 1px solid #dee2e6;
border-bottom: none;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}

.exelearning-download {
position: relative;
display: inline-flex;
align-items: stretch;
background: #0073aa;
border-radius: 4px;
color: #fff;
overflow: visible;
}

.exelearning-download[data-busy="1"] {
opacity: 0.7;
pointer-events: none;
}

.exelearning-download__primary,
.exelearning-download__toggle,
.exelearning-download__item {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 10px;
background: transparent;
color: #fff;
text-decoration: none;
border: 0;
cursor: pointer;
font-family: inherit;
font-size: 13px;
font-weight: 400;
line-height: 1.2;
}

.exelearning-download .dashicons {
font-size: 16px;
width: 16px;
height: 16px;
line-height: 1;
}

.exelearning-download__primary:hover,
.exelearning-download__toggle:hover {
background: rgba(0, 0, 0, 0.15);
color: #fff;
}

.exelearning-download__toggle {
border-left: 1px solid rgba(255, 255, 255, 0.25);
padding: 6px 8px;
}

.exelearning-download__menu {
position: absolute;
top: 100%;
right: 0;
margin: 4px 0 0;
padding: 4px 0;
list-style: none;
background: #fff;
color: #1d2327;
border: 1px solid #c3c4c7;
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
min-width: 220px;
z-index: 1000;
}

.exelearning-download__menu .exelearning-download__item {
width: 100%;
color: #1d2327;
padding: 6px 12px;
background: transparent;
text-align: left;
}

.exelearning-download__menu .exelearning-download__item:hover {
background: #f0f0f1;
color: #1d2327;
}

.exelearning-download__status {
font-size: 12px;
margin-left: 10px;
color: #50575e;
align-self: center;
}

/* Error message */
.exelearning-error {
padding: 15px;
Expand All @@ -53,3 +153,43 @@
border-radius: 4px;
color: #721c24;
}

.exelearning-download__item--disabled,
.exelearning-download__primary.exelearning-download__item--disabled {
opacity: 0.55;
cursor: not-allowed;
pointer-events: none;
}

/* Busy spinner — replaces the previous "Preparing download…" inline text.
When the orchestrator JS is generating an export it sets data-busy="1" on
the container; we overlay a small spinner over the primary action and
dim it. Cleared automatically as soon as JS removes the attribute. */
.exelearning-download[data-busy="1"] {
opacity: 1;
pointer-events: none;
}
.exelearning-download[data-busy="1"] .exelearning-download__primary {
position: relative;
}
.exelearning-download[data-busy="1"] .exelearning-download__primary > * {
visibility: hidden;
}
.exelearning-download[data-busy="1"] .exelearning-download__primary::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 14px;
height: 14px;
margin: -7px 0 0 -7px;
border: 2px solid currentColor;
border-top-color: transparent;
border-radius: 50%;
opacity: 0.85;
animation: exelearning-spin 0.7s linear infinite;
visibility: visible;
}
@keyframes exelearning-spin {
to { transform: rotate(360deg); }
}
50 changes: 50 additions & 0 deletions assets/js/elp-upload.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,23 @@
var PanelBody = wp.components.PanelBody;
var RangeControl = wp.components.RangeControl;
var ToggleControl = wp.components.ToggleControl;
var CheckboxControl = wp.components.CheckboxControl;
var useRef = wp.element.useRef;
var useEffect = wp.element.useEffect;
var useState = wp.element.useState;

var TEACHER_MODE_STYLE_ID = 'exelearning-teacher-mode-style';
var TEACHER_MODE_CSS = '#teacher-mode-toggler-wrapper { visibility: hidden !important; }';

var DOWNLOAD_FORMAT_DEFINITIONS = [
{ id: 'elpx', labelKey: 'Download .elpx' },
{ id: 'html5', labelKey: 'Web (_web.zip)' },
{ id: 'scorm12', labelKey: 'SCORM 1.2 (_scorm.zip)' },
{ id: 'ims', labelKey: 'IMS Package (_ims.zip)' },
{ id: 'epub3', labelKey: 'EPUB3 (.epub)' }
];
var DEFAULT_DOWNLOAD_FORMATS = DOWNLOAD_FORMAT_DEFINITIONS.map( function( f ) { return f.id; } );

registerBlockType( 'exelearning/elp-upload', {
title: 'eXeLearning',
icon: el( 'svg', { width: 24, height: 24, viewBox: '0 0 350 230', xmlns: 'http://www.w3.org/2000/svg' },
Expand Down Expand Up @@ -63,6 +73,14 @@
type: 'boolean',
default: true,
},
showDownload: {
type: 'boolean',
default: false,
},
downloadFormats: {
type: 'array',
default: DEFAULT_DOWNLOAD_FORMATS,
},
},

edit: function( props ) {
Expand Down Expand Up @@ -239,6 +257,38 @@
onClick: onEditInExeLearning,
style: { marginTop: '10px', width: '100%', justifyContent: 'center' }
}, __( 'Edit in eXeLearning', 'exelearning' ) )
),
el( PanelBody, { title: __( 'Download options', 'exelearning' ), initialOpen: false },
el( ToggleControl, {
label: __( 'Show download button', 'exelearning' ),
checked: attributes.showDownload !== false,
onChange: function( value ) {
setAttributes( { showDownload: value } );
},
}),
attributes.showDownload !== false && el( Fragment, null,
el( 'p', { style: { marginTop: '10px', marginBottom: '6px', fontWeight: 600 } },
__( 'Available formats', 'exelearning' )
),
DOWNLOAD_FORMAT_DEFINITIONS.map( function( fmt ) {
var current = Array.isArray( attributes.downloadFormats ) ? attributes.downloadFormats : DEFAULT_DOWNLOAD_FORMATS;
var checked = current.indexOf( fmt.id ) !== -1;
return el( CheckboxControl, {
key: fmt.id,
label: __( fmt.labelKey, 'exelearning' ),
checked: checked,
onChange: function( value ) {
var next = current.filter( function( id ) { return id !== fmt.id; } );
if ( value ) {
next.push( fmt.id );
}
// Preserve canonical order.
next = DEFAULT_DOWNLOAD_FORMATS.filter( function( id ) { return next.indexOf( id ) !== -1; } );
setAttributes( { downloadFormats: next } );
}
});
})
)
)
),
// Block Controls (toolbar)
Expand Down
55 changes: 54 additions & 1 deletion assets/js/wp-exe-bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

const config = window.__WP_EXE_CONFIG__ || {};
const targetOrigin = window.__EXE_EMBEDDING_CONFIG__?.parentOrigin || '*';
const rawCapabilities = [ 'WP_REQUEST_SAVE', 'GET_PROJECT_INFO', 'CONFIGURE' ];
const rawCapabilities = [ 'WP_REQUEST_SAVE', 'WP_REQUEST_EXPORT', 'GET_PROJECT_INFO', 'CONFIGURE' ];
let documentLoadedNotified = false;

function notifyParent( type, data ) {
Expand Down Expand Up @@ -93,6 +93,41 @@
};
}

async function exportFormat( format ) {
const app = await getApp();
const project = app.project;
const yjsBridge = project?._yjsBridge;
if ( ! window.SharedExporters?.quickExport ) {
throw new Error( 'SharedExporters bundle not loaded' );
}
if ( ! yjsBridge?.documentManager ) {
throw new Error( 'Document not ready' );
}

const result = await window.SharedExporters.quickExport(
format,
yjsBridge.documentManager,
yjsBridge.assetCache || null,
yjsBridge.resourceFetcher || null,
{},
yjsBridge.assetManager || null
);
if ( ! result?.success || ! result?.data ) {
throw new Error( result?.error || 'Export failed' );
}

const mime = ( format === 'epub3' || format === 'epub' )
? 'application/epub+zip'
: 'application/zip';
const blob = new Blob( [ result.data ], { type: mime } );
const bytes = await blob.arrayBuffer();
return {
bytes,
filename: result.filename || ( 'project.' + format ),
mimeType: blob.type,
};
}

async function getProjectInfo() {
const app = await getApp();
const project = app.project;
Expand Down Expand Up @@ -196,6 +231,24 @@
break;
}

case 'WP_REQUEST_EXPORT': {
const format = String( message.data?.format || message.format || '' );
if ( ! format ) {
throw new Error( 'Missing export format' );
}
const exported = await exportFormat( format );
postProtocolMessage( {
type: 'WP_EXPORT_FILE',
requestId: message.requestId,
format,
bytes: exported.bytes,
filename: exported.filename,
mimeType: exported.mimeType,
size: exported.bytes.byteLength,
} );
break;
}

case 'GET_PROJECT_INFO': {
const info = await getProjectInfo();
postProtocolMessage( {
Expand Down
Loading