Release 2.16.0
This commit is contained in:
1 parent
821593dd8f
commit
3514d514a3
36 files changed
+2329
-346
No files matched your search
@@ -0,0 +1,450 @@
|
||||
export function createSlideLayoutEditor(options) {
|
||||
var helpers = window.pulseRegionLayout;
|
||||
var stage = options.stage;
|
||||
var modal = document.getElementById('slide-layout-modal');
|
||||
var workspace = document.getElementById('slide-layout-workspace');
|
||||
var regionSelect = document.getElementById('slide-layout-region');
|
||||
var hidden = document.getElementById('slide-layouts-json');
|
||||
var openButton = document.getElementById('adjust-slide-layout');
|
||||
var animationButton = document.getElementById('adjust-slide-animations');
|
||||
var undoButton = document.getElementById('slide-layout-undo');
|
||||
var redoButton = document.getElementById('slide-layout-redo');
|
||||
var resetButton = document.getElementById('slide-layout-reset');
|
||||
var snapInput = document.getElementById('slide-layout-snap');
|
||||
var sharedButton = document.getElementById('save-layout-to-template');
|
||||
var saveStatus = document.getElementById('slide-layout-save-status');
|
||||
var sharedSaving = false;
|
||||
var sharedBaseline = null;
|
||||
var properties = document.getElementById('slide-layout-fields');
|
||||
var layer = document.createElement('div');
|
||||
layer.className = 'slide-layout-layer';
|
||||
layer.hidden = true;
|
||||
stage.appendChild(layer);
|
||||
var fields = {};
|
||||
['x', 'y', 'width', 'height', 'z_index', 'lock_ratio'].forEach(function (field) {
|
||||
fields[field] = properties.querySelector('[name="region_' + (field === 'z_index' ? 'z' : field) + '[]"]');
|
||||
fields[field].id = 'slide-layout-' + field;
|
||||
});
|
||||
var layouts = Object.create(null);
|
||||
var selectedKey = '';
|
||||
var history = [];
|
||||
var historyIndex = 0;
|
||||
var baseline = '';
|
||||
var home = stage.parentNode;
|
||||
var nextSibling = stage.nextSibling;
|
||||
var active = false;
|
||||
var applied = false;
|
||||
var drag = null;
|
||||
var guides = [];
|
||||
|
||||
async function readResponse(response) {
|
||||
var data;
|
||||
try { data = await response.json(); } catch (_error) { throw new Error('Unable to save the shared layout. Check your connection and sign-in status.'); }
|
||||
if (!response.ok) throw new Error(data.error || 'Unable to save the shared layout.');
|
||||
return data;
|
||||
}
|
||||
if (sharedButton) sharedButton.addEventListener('click', async function () {
|
||||
if (sharedSaving) return;
|
||||
if (Object.keys(fields).some(function (field) { return !fields[field].reportValidity(); })) return;
|
||||
sharedSaving = true;
|
||||
modal.setAttribute('aria-busy', 'true');
|
||||
var controls = Array.from(modal.querySelectorAll('button, input, select')).map(function (input) {
|
||||
var state = { input: input, disabled: input.disabled };
|
||||
input.disabled = true;
|
||||
return state;
|
||||
});
|
||||
saveStatus.classList.remove('text-danger');
|
||||
saveStatus.textContent = 'Checking shared template...';
|
||||
try {
|
||||
var url = '/templates/' + encodeURIComponent(template().id) + '/layout';
|
||||
var current = await readResponse(await fetch(url, { headers: { Accept: 'application/json' } }));
|
||||
if (JSON.stringify(helpers.getTemplateLayoutSnapshot(current.template)) !== JSON.stringify(sharedBaseline)) {
|
||||
throw new Error('The shared template layout has changed. Reload the slide before saving to the template.');
|
||||
}
|
||||
var count = Number(current.slideCount) || 0;
|
||||
if (!window.confirm('Save this layout to "' + current.template.name + '"? It is used by ' + count + ' slide' + (count === 1 ? '' : 's') + '. Other slides keep their explicit overrides. Unsaved slide content is not saved.')) {
|
||||
saveStatus.textContent = '';
|
||||
return;
|
||||
}
|
||||
saveStatus.textContent = 'Saving shared layout...';
|
||||
var result = await readResponse(await fetch(url, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
body: JSON.stringify({ slideId: options.slideId || null, baseline: sharedBaseline, regions: helpers.getTemplateLayoutSnapshot(Object.assign({}, template(), { regions: regions().map(effective) })).regions })
|
||||
}));
|
||||
options.updateTemplate(result.template);
|
||||
var inheritedLayouts = Object.create(null);
|
||||
Object.keys(layouts).forEach(function (key) {
|
||||
var animation = helpers.retainLayoutAnimation(layouts[key]);
|
||||
if (animation) inheritedLayouts[key] = animation;
|
||||
});
|
||||
layouts = inheritedLayouts;
|
||||
baseline = snapshot();
|
||||
sharedBaseline = helpers.getTemplateLayoutSnapshot(template());
|
||||
history = [baseline];
|
||||
historyIndex = 0;
|
||||
applied = true;
|
||||
options.markEdited();
|
||||
hidden.value = baseline;
|
||||
sharedSaving = false;
|
||||
saveStatus.textContent = '';
|
||||
options.requestPreviewRender();
|
||||
window.pulseModal.hide(modal);
|
||||
if (typeof window.showToast === 'function') window.showToast('Shared template layout saved.', 'success');
|
||||
} catch (error) {
|
||||
saveStatus.textContent = error.message;
|
||||
saveStatus.classList.add('text-danger');
|
||||
} finally {
|
||||
sharedSaving = false;
|
||||
modal.removeAttribute('aria-busy');
|
||||
controls.forEach(function (state) { state.input.disabled = state.disabled; });
|
||||
render();
|
||||
}
|
||||
});
|
||||
modal.addEventListener('hide.bs.modal', function (event) {
|
||||
if (sharedSaving) event.preventDefault();
|
||||
});
|
||||
|
||||
function template() { return options.getTemplate(); }
|
||||
function canvas() {
|
||||
var current = template();
|
||||
return { width: Number(current && current.canvas_size_width) || 1920, height: Number(current && current.canvas_size_height) || 1080 };
|
||||
}
|
||||
function regions() { return template() && template().regions || []; }
|
||||
function selected() {
|
||||
return regions().find(function (region) { return region.region_key === selectedKey; });
|
||||
}
|
||||
function effective(region) {
|
||||
return helpers.applyLayout(region, { layout: layouts[region.region_key] }, canvas());
|
||||
}
|
||||
function snapshot() { return JSON.stringify(layouts); }
|
||||
function commit() {
|
||||
var value = snapshot();
|
||||
if (history[historyIndex] === value) return;
|
||||
history = history.slice(0, historyIndex + 1);
|
||||
history.push(value);
|
||||
historyIndex = history.length - 1;
|
||||
}
|
||||
function update() {
|
||||
hidden.value = snapshot();
|
||||
render();
|
||||
options.requestPreviewRender();
|
||||
}
|
||||
function change(box) {
|
||||
if (!selected()) return;
|
||||
var geometry = { x: box.x, y: box.y, width: box.width, height: box.height };
|
||||
layouts[selectedKey] = helpers.normalizeLayout(Object.assign({}, layouts[selectedKey], geometry), canvas());
|
||||
update();
|
||||
}
|
||||
function changeSettings(patch) {
|
||||
if (!selected()) return;
|
||||
var box = effective(selected());
|
||||
var next = helpers.normalizeLayout(Object.assign({ x: box.x, y: box.y, width: box.width, height: box.height }, layouts[selectedKey], patch), canvas());
|
||||
if (!next) return false;
|
||||
layouts[selectedKey] = next;
|
||||
commit();
|
||||
update();
|
||||
return true;
|
||||
}
|
||||
function render() {
|
||||
openButton.disabled = active || !regions().length;
|
||||
animationButton.disabled = active || !regions().length;
|
||||
if (!active || sharedSaving) return;
|
||||
if (!selected()) selectedKey = regions().length ? regions()[0].region_key : '';
|
||||
var region = selected();
|
||||
var box = region && effective(region);
|
||||
Object.keys(fields).forEach(function (field) {
|
||||
fields[field].disabled = !box;
|
||||
fields[field].value = box && box[field] !== undefined && box[field] !== null ? box[field] : '';
|
||||
fields[field].setCustomValidity('');
|
||||
if (field === 'x' || field === 'width') fields[field].max = canvas().width;
|
||||
if (field === 'y' || field === 'height') fields[field].max = canvas().height;
|
||||
});
|
||||
regionSelect.value = selectedKey;
|
||||
undoButton.disabled = historyIndex === 0;
|
||||
redoButton.disabled = historyIndex >= history.length - 1;
|
||||
resetButton.disabled = !Object.hasOwn(layouts, selectedKey);
|
||||
document.getElementById('slide-layout-reset-all').disabled = !Object.keys(layouts).length;
|
||||
layer.replaceChildren();
|
||||
regions().map(effective).sort(function (first, second) {
|
||||
return Number(first.z_index || 1) - Number(second.z_index || 1);
|
||||
}).forEach(function (item) {
|
||||
var itemBox = effective(item);
|
||||
var hit = document.createElement('div');
|
||||
hit.className = 'designer-rect slide-layout-hit' + (item.region_key === selectedKey ? ' selected' : '');
|
||||
hit.dataset.regionKey = item.region_key;
|
||||
hit.setAttribute('role', 'button');
|
||||
hit.setAttribute('aria-label', item.label || item.region_key);
|
||||
hit.setAttribute('aria-pressed', String(item.region_key === selectedKey));
|
||||
hit.tabIndex = 0;
|
||||
hit.style.left = itemBox.x / canvas().width * 100 + '%';
|
||||
hit.style.top = itemBox.y / canvas().height * 100 + '%';
|
||||
hit.style.width = itemBox.width / canvas().width * 100 + '%';
|
||||
hit.style.height = itemBox.height / canvas().height * 100 + '%';
|
||||
var label = document.createElement('span');
|
||||
label.className = 'designer-rect-label';
|
||||
label.textContent = item.label || item.region_key;
|
||||
hit.appendChild(label);
|
||||
if (item.region_key === selectedKey) {
|
||||
['nw', 'ne', 'sw', 'se'].forEach(function (direction) {
|
||||
var handle = document.createElement('span');
|
||||
handle.className = 'resize-handle ' + direction;
|
||||
handle.dataset.direction = direction;
|
||||
hit.appendChild(handle);
|
||||
});
|
||||
}
|
||||
layer.appendChild(hit);
|
||||
});
|
||||
guides.forEach(function (guide) {
|
||||
var line = document.createElement('div');
|
||||
line.className = 'layout-alignment-guide layout-alignment-guide--' + guide.axis;
|
||||
line.style[guide.axis === 'x' ? 'left' : 'top'] = guide.position / canvas()[guide.axis === 'x' ? 'width' : 'height'] * 100 + '%';
|
||||
layer.appendChild(line);
|
||||
});
|
||||
}
|
||||
|
||||
function open() {
|
||||
if (active || !regions().length || !window.pulseModal) return;
|
||||
baseline = snapshot();
|
||||
sharedBaseline = helpers.getTemplateLayoutSnapshot(template());
|
||||
saveStatus.textContent = '';
|
||||
saveStatus.classList.remove('text-danger');
|
||||
history = [baseline];
|
||||
historyIndex = 0;
|
||||
applied = false;
|
||||
active = true;
|
||||
home = stage.parentNode;
|
||||
nextSibling = stage.nextSibling;
|
||||
regionSelect.replaceChildren();
|
||||
regions().forEach(function (region) {
|
||||
var option = document.createElement('option');
|
||||
option.value = region.region_key;
|
||||
option.textContent = region.label || region.region_key;
|
||||
regionSelect.appendChild(option);
|
||||
});
|
||||
workspace.appendChild(stage);
|
||||
stage.classList.add('slide-layout-stage');
|
||||
stage.tabIndex = 0;
|
||||
layer.hidden = false;
|
||||
var main = document.querySelector('.app-main');
|
||||
var sidebarEdge = window.innerWidth >= 992 && main ? Math.max(0, main.getBoundingClientRect().left) : 0;
|
||||
modal.style.setProperty('--slide-layout-left', sidebarEdge + 'px');
|
||||
render();
|
||||
window.pulseModal.show(modal);
|
||||
}
|
||||
|
||||
modal.addEventListener('shown.bs.modal', function () {
|
||||
options.requestPreviewRender();
|
||||
stage.focus();
|
||||
});
|
||||
modal.addEventListener('hidden.bs.modal', function () {
|
||||
if (!applied) layouts = JSON.parse(baseline);
|
||||
active = false;
|
||||
drag = null;
|
||||
guides = [];
|
||||
layer.hidden = true;
|
||||
stage.classList.remove('slide-layout-stage');
|
||||
stage.removeAttribute('tabindex');
|
||||
home.insertBefore(stage, nextSibling);
|
||||
update();
|
||||
openButton.focus();
|
||||
});
|
||||
openButton.addEventListener('click', open);
|
||||
modal.querySelectorAll('[data-bs-dismiss="modal"]').forEach(function (button) {
|
||||
button.removeAttribute('data-bs-dismiss');
|
||||
button.addEventListener('click', function () { window.pulseModal.hide(modal); });
|
||||
});
|
||||
document.getElementById('slide-layout-apply').addEventListener('click', function () {
|
||||
applied = true;
|
||||
if (snapshot() !== baseline) options.markEdited();
|
||||
hidden.value = snapshot();
|
||||
window.pulseModal.hide(modal);
|
||||
});
|
||||
regionSelect.addEventListener('change', function () {
|
||||
selectedKey = regionSelect.value;
|
||||
guides = [];
|
||||
render();
|
||||
});
|
||||
function restoreHistory(offset) {
|
||||
var index = historyIndex + offset;
|
||||
if (index < 0 || index >= history.length) return;
|
||||
historyIndex = index;
|
||||
layouts = JSON.parse(history[index]);
|
||||
guides = [];
|
||||
update();
|
||||
}
|
||||
undoButton.addEventListener('click', function () { restoreHistory(-1); });
|
||||
redoButton.addEventListener('click', function () { restoreHistory(1); });
|
||||
resetButton.addEventListener('click', function () {
|
||||
delete layouts[selectedKey];
|
||||
commit();
|
||||
update();
|
||||
});
|
||||
document.getElementById('slide-layout-reset-all').addEventListener('click', function () {
|
||||
layouts = Object.create(null);
|
||||
commit();
|
||||
update();
|
||||
});
|
||||
Object.keys(fields).forEach(function (field) {
|
||||
fields[field].addEventListener('keydown', function (event) {
|
||||
if (event.key === 'Enter') { event.preventDefault(); fields[field].blur(); }
|
||||
});
|
||||
fields[field].addEventListener('change', function () {
|
||||
if (field === 'lock_ratio') {
|
||||
var ratioValue = fields[field].value.trim().replace(/\s+/g, '');
|
||||
var parts = ratioValue.split(':').map(Number);
|
||||
var patch = { lock_ratio: ratioValue };
|
||||
if (/^\d+:\d+$/.test(ratioValue) && parts[0] > 0 && parts[1] > 0) {
|
||||
var current = effective(selected());
|
||||
var fitted = helpers.fitLayout({ x: current.x, y: current.y, width: current.width, height: current.width * parts[1] / parts[0] }, canvas(), parts[0] / parts[1], 'se');
|
||||
Object.assign(patch, fitted);
|
||||
}
|
||||
if (!changeSettings(patch)) {
|
||||
fields[field].setCustomValidity('Enter a positive ratio such as 16:9, or leave blank.');
|
||||
fields[field].reportValidity();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!selected() || fields[field].value === '' || !Number.isFinite(fields[field].valueAsNumber)) {
|
||||
render();
|
||||
return;
|
||||
}
|
||||
var box = effective(selected());
|
||||
box[field] = fields[field].valueAsNumber;
|
||||
if (field === 'z_index') { changeSettings({ z_index: box.z_index }); return; }
|
||||
var ratio = String(box.lock_ratio || '').split(':').map(Number);
|
||||
if (ratio[0] > 0 && ratio[1] > 0) {
|
||||
if (field === 'width') box.height = Math.round(box.width * ratio[1] / ratio[0]);
|
||||
if (field === 'height') box.width = Math.round(box.height * ratio[0] / ratio[1]);
|
||||
}
|
||||
change(helpers.fitLayout(box, canvas(), ratio[0] / ratio[1], 'se'));
|
||||
commit();
|
||||
render();
|
||||
});
|
||||
});
|
||||
stage.addEventListener('pointerdown', function (event) {
|
||||
if (!active || sharedSaving || event.button !== 0) return;
|
||||
var hit = event.target.closest('.slide-layout-hit');
|
||||
if (!hit) return;
|
||||
event.preventDefault();
|
||||
selectedKey = hit.dataset.regionKey;
|
||||
drag = {
|
||||
pointerId: event.pointerId,
|
||||
startX: event.clientX, startY: event.clientY,
|
||||
box: effective(selected()),
|
||||
direction: event.target.dataset.direction || '',
|
||||
snapshot: snapshot()
|
||||
};
|
||||
stage.setPointerCapture(event.pointerId);
|
||||
stage.focus({ preventScroll: true });
|
||||
render();
|
||||
});
|
||||
stage.addEventListener('pointermove', function (event) {
|
||||
if (sharedSaving || !drag || event.pointerId !== drag.pointerId) return;
|
||||
var bounds = stage.getBoundingClientRect();
|
||||
var dx = (event.clientX - drag.startX) * canvas().width / bounds.width;
|
||||
var dy = (event.clientY - drag.startY) * canvas().height / bounds.height;
|
||||
if (Math.abs(event.clientX - drag.startX) + Math.abs(event.clientY - drag.startY) < 2) return;
|
||||
var box = Object.assign({}, drag.box);
|
||||
var direction = drag.direction;
|
||||
var aspect = 0;
|
||||
if (direction) {
|
||||
if (direction.includes('w')) { box.x += dx; box.width -= dx; }
|
||||
if (direction.includes('e')) box.width += dx;
|
||||
if (direction.includes('n')) { box.y += dy; box.height -= dy; }
|
||||
if (direction.includes('s')) box.height += dy;
|
||||
var ratio = String(effective(selected()).lock_ratio || '').split(':').map(Number);
|
||||
if (ratio[0] > 0 && ratio[1] > 0) {
|
||||
aspect = ratio[0] / ratio[1];
|
||||
if (Math.abs(dy * aspect) > Math.abs(dx)) box.width = Math.max(1, box.height * aspect);
|
||||
box.height = Math.max(1, box.width / aspect);
|
||||
if (direction.includes('w')) box.x = drag.box.x + drag.box.width - box.width;
|
||||
if (direction.includes('n')) box.y = drag.box.y + drag.box.height - box.height;
|
||||
}
|
||||
box = helpers.fitLayout(box, canvas(), aspect, direction);
|
||||
} else {
|
||||
box.x = Math.max(0, Math.min(canvas().width - box.width, box.x + dx));
|
||||
box.y = Math.max(0, Math.min(canvas().height - box.height, box.y + dy));
|
||||
}
|
||||
var peers = regions().filter(function (region) { return region.region_key !== selectedKey; }).map(effective);
|
||||
var snapped = helpers.snapBox(box, peers, canvas(), snapInput.checked && !event.altKey ? 6 * canvas().width / bounds.width : -1, direction, aspect);
|
||||
guides = snapped.guides;
|
||||
change(snapped.box);
|
||||
});
|
||||
function endDrag(event) {
|
||||
if (!drag || event.pointerId !== drag.pointerId) return;
|
||||
if (event.type === 'pointercancel') layouts = JSON.parse(drag.snapshot);
|
||||
if (stage.hasPointerCapture(event.pointerId)) stage.releasePointerCapture(event.pointerId);
|
||||
drag = null;
|
||||
guides = [];
|
||||
commit();
|
||||
update();
|
||||
}
|
||||
stage.addEventListener('pointerup', endDrag);
|
||||
stage.addEventListener('pointercancel', endDrag);
|
||||
stage.addEventListener('keydown', function (event) {
|
||||
if (!active || sharedSaving) return;
|
||||
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'z') {
|
||||
event.preventDefault();
|
||||
restoreHistory(event.shiftKey ? 1 : -1);
|
||||
return;
|
||||
}
|
||||
var hit = event.target.closest('.slide-layout-hit');
|
||||
if (hit) selectedKey = hit.dataset.regionKey;
|
||||
if (!selected()) return;
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
stage.focus();
|
||||
render();
|
||||
return;
|
||||
}
|
||||
var movements = { ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, -1], ArrowDown: [0, 1] };
|
||||
var movement = movements[event.key];
|
||||
if (!movement) return;
|
||||
event.preventDefault();
|
||||
var box = effective(selected());
|
||||
var step = event.shiftKey ? 10 : 1;
|
||||
box.x = Math.max(0, Math.min(canvas().width - box.width, box.x + movement[0] * step));
|
||||
box.y = Math.max(0, Math.min(canvas().height - box.height, box.y + movement[1] * step));
|
||||
change(box);
|
||||
commit();
|
||||
render();
|
||||
stage.focus();
|
||||
});
|
||||
return {
|
||||
apply: effective,
|
||||
refresh: render,
|
||||
getAnimationOverrides: function () {
|
||||
var overrides = Object.create(null);
|
||||
Object.keys(layouts).forEach(function (key) {
|
||||
if (layouts[key] && layouts[key].animation_json !== undefined) overrides[key] = layouts[key].animation_json;
|
||||
});
|
||||
return overrides;
|
||||
},
|
||||
getAnimation: function (regionKey) {
|
||||
var region = regions().find(function (item) { return item.region_key === regionKey; });
|
||||
var override = layouts[regionKey] && layouts[regionKey].animation_json;
|
||||
return override !== undefined ? override : String(region && region.animation_json || '{}');
|
||||
},
|
||||
setAnimationOverrides: function (overrides) {
|
||||
regions().forEach(function (region) {
|
||||
var key = region.region_key;
|
||||
var current = layouts[key] ? Object.assign({}, layouts[key]) : {};
|
||||
delete current.animation_json;
|
||||
if (Object.prototype.hasOwnProperty.call(overrides, key)) current.animation_json = overrides[key];
|
||||
if (Object.keys(current).length) layouts[key] = helpers.normalizeLayout(current, canvas());
|
||||
else delete layouts[key];
|
||||
});
|
||||
commit();
|
||||
update();
|
||||
},
|
||||
reset: function (content) {
|
||||
layouts = Object.create(null);
|
||||
regions().forEach(function (region) {
|
||||
var layout = helpers.normalizeLayout(content && content[region.region_key] && content[region.region_key].layout, canvas());
|
||||
if (layout) layouts[region.region_key] = layout;
|
||||
});
|
||||
hidden.value = snapshot();
|
||||
render();
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user