- Add JavaScript to load user profile from /api/auth/me endpoint - Save access_token to localStorage/sessionStorage on login - Update user menu to show actual user name and email - Toggle Sign in/Sign out based on authentication state - Add IDs to user menu elements for dynamic updates
1087 lines
31 KiB
JavaScript
1087 lines
31 KiB
JavaScript
/* =============================================================================
|
|
GB SLIDES - PowerPoint-like Presentation JavaScript
|
|
General Bots Suite Component
|
|
============================================================================= */
|
|
|
|
(function () {
|
|
"use strict";
|
|
|
|
const CONFIG = {
|
|
CANVAS_WIDTH: 960,
|
|
CANVAS_HEIGHT: 540,
|
|
MAX_HISTORY: 50,
|
|
AUTOSAVE_DELAY: 3000,
|
|
WS_RECONNECT_DELAY: 5000,
|
|
MIN_ELEMENT_SIZE: 20,
|
|
};
|
|
|
|
const state = {
|
|
presentationId: null,
|
|
presentationName: "Untitled Presentation",
|
|
slides: [],
|
|
currentSlideIndex: 0,
|
|
selectedElement: null,
|
|
clipboard: null,
|
|
history: [],
|
|
historyIndex: -1,
|
|
zoom: 1,
|
|
collaborators: [],
|
|
ws: null,
|
|
isDragging: false,
|
|
isResizing: false,
|
|
isRotating: false,
|
|
dragStart: null,
|
|
resizeHandle: null,
|
|
isDirty: false,
|
|
autoSaveTimer: null,
|
|
isPresenting: false,
|
|
theme: null,
|
|
};
|
|
|
|
const elements = {
|
|
container: null,
|
|
sidebar: null,
|
|
thumbnails: null,
|
|
canvas: null,
|
|
canvasContainer: null,
|
|
selectionHandles: null,
|
|
propertiesPanel: null,
|
|
presentationName: null,
|
|
collaborators: null,
|
|
contextMenu: null,
|
|
slideContextMenu: null,
|
|
cursorIndicators: null,
|
|
};
|
|
|
|
function init() {
|
|
cacheElements();
|
|
bindEvents();
|
|
loadFromUrlParams();
|
|
connectWebSocket();
|
|
createNewPresentation();
|
|
}
|
|
|
|
function cacheElements() {
|
|
elements.container = document.querySelector(".slides-container");
|
|
elements.sidebar = document.getElementById("slides-sidebar");
|
|
elements.thumbnails = document.getElementById("slide-thumbnails");
|
|
elements.canvas = document.getElementById("slide-canvas");
|
|
elements.canvasContainer = document.getElementById("canvas-container");
|
|
elements.selectionHandles = document.getElementById("selection-handles");
|
|
elements.propertiesPanel = document.getElementById("properties-panel");
|
|
elements.presentationName = document.getElementById("presentation-name");
|
|
elements.collaborators = document.getElementById("collaborators");
|
|
elements.contextMenu = document.getElementById("context-menu");
|
|
elements.slideContextMenu = document.getElementById("slide-context-menu");
|
|
elements.cursorIndicators = document.getElementById("cursor-indicators");
|
|
}
|
|
|
|
function bindEvents() {
|
|
elements.canvas.addEventListener("mousedown", handleCanvasMouseDown);
|
|
elements.canvas.addEventListener("dblclick", handleCanvasDoubleClick);
|
|
document.addEventListener("mousemove", handleMouseMove);
|
|
document.addEventListener("mouseup", handleMouseUp);
|
|
document.addEventListener("keydown", handleKeyDown);
|
|
document.addEventListener("click", hideContextMenus);
|
|
elements.canvas.addEventListener("contextmenu", handleContextMenu);
|
|
|
|
const handles = elements.selectionHandles.querySelectorAll(
|
|
".handle, .rotate-handle",
|
|
);
|
|
handles.forEach((handle) => {
|
|
handle.addEventListener("mousedown", handleResizeStart);
|
|
});
|
|
|
|
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
}
|
|
|
|
function createNewPresentation() {
|
|
const titleSlide = createSlide("title");
|
|
state.slides = [titleSlide];
|
|
state.currentSlideIndex = 0;
|
|
state.theme = createDefaultTheme();
|
|
renderThumbnails();
|
|
renderCurrentSlide();
|
|
updateSlideCounter();
|
|
}
|
|
|
|
function createSlide(layout) {
|
|
const slide = {
|
|
id: generateId(),
|
|
layout: layout,
|
|
elements: [],
|
|
background: {
|
|
bg_type: "solid",
|
|
color: "#ffffff",
|
|
},
|
|
notes: null,
|
|
transition: {
|
|
transition_type: "fade",
|
|
duration: 0.5,
|
|
},
|
|
};
|
|
|
|
switch (layout) {
|
|
case "title":
|
|
slide.elements.push(
|
|
createTextElement(100, 200, 760, 100, "Presentation Title", {
|
|
fontSize: 48,
|
|
fontWeight: "bold",
|
|
textAlign: "center",
|
|
color: "#1e293b",
|
|
}),
|
|
);
|
|
slide.elements.push(
|
|
createTextElement(100, 320, 760, 50, "Subtitle or Author Name", {
|
|
fontSize: 24,
|
|
textAlign: "center",
|
|
color: "#64748b",
|
|
}),
|
|
);
|
|
break;
|
|
case "title-content":
|
|
slide.elements.push(
|
|
createTextElement(50, 40, 860, 60, "Slide Title", {
|
|
fontSize: 36,
|
|
fontWeight: "bold",
|
|
color: "#1e293b",
|
|
}),
|
|
);
|
|
slide.elements.push(
|
|
createTextElement(
|
|
50,
|
|
120,
|
|
860,
|
|
400,
|
|
"• Click to add content\n• Add your bullet points here",
|
|
{
|
|
fontSize: 20,
|
|
color: "#374151",
|
|
lineHeight: 1.6,
|
|
},
|
|
),
|
|
);
|
|
break;
|
|
case "two-column":
|
|
slide.elements.push(
|
|
createTextElement(50, 40, 860, 60, "Slide Title", {
|
|
fontSize: 36,
|
|
fontWeight: "bold",
|
|
color: "#1e293b",
|
|
}),
|
|
);
|
|
slide.elements.push(
|
|
createTextElement(50, 120, 410, 400, "Left column content", {
|
|
fontSize: 18,
|
|
color: "#374151",
|
|
}),
|
|
);
|
|
slide.elements.push(
|
|
createTextElement(500, 120, 410, 400, "Right column content", {
|
|
fontSize: 18,
|
|
color: "#374151",
|
|
}),
|
|
);
|
|
break;
|
|
case "section":
|
|
slide.elements.push(
|
|
createTextElement(100, 220, 760, 100, "Section Title", {
|
|
fontSize: 48,
|
|
fontWeight: "bold",
|
|
textAlign: "center",
|
|
color: "#1e293b",
|
|
}),
|
|
);
|
|
break;
|
|
case "blank":
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return slide;
|
|
}
|
|
|
|
function createTextElement(x, y, width, height, text, style) {
|
|
return {
|
|
id: generateId(),
|
|
element_type: "text",
|
|
x: x,
|
|
y: y,
|
|
width: width,
|
|
height: height,
|
|
rotation: 0,
|
|
content: { text: text },
|
|
style: {
|
|
fontFamily: style.fontFamily || "Inter",
|
|
fontSize: style.fontSize || 16,
|
|
fontWeight: style.fontWeight || "normal",
|
|
fontStyle: style.fontStyle || "normal",
|
|
textAlign: style.textAlign || "left",
|
|
verticalAlign: style.verticalAlign || "top",
|
|
color: style.color || "#000000",
|
|
lineHeight: style.lineHeight || 1.4,
|
|
...style,
|
|
},
|
|
animations: [],
|
|
z_index: 1,
|
|
locked: false,
|
|
};
|
|
}
|
|
|
|
function createShapeElement(x, y, width, height, shapeType, style) {
|
|
return {
|
|
id: generateId(),
|
|
element_type: "shape",
|
|
x: x,
|
|
y: y,
|
|
width: width,
|
|
height: height,
|
|
rotation: 0,
|
|
content: { shape_type: shapeType },
|
|
style: {
|
|
fill: style.fill || "#3b82f6",
|
|
stroke: style.stroke || "none",
|
|
strokeWidth: style.strokeWidth || 0,
|
|
opacity: style.opacity || 1,
|
|
borderRadius: style.borderRadius || 0,
|
|
...style,
|
|
},
|
|
animations: [],
|
|
z_index: 1,
|
|
locked: false,
|
|
};
|
|
}
|
|
|
|
function createImageElement(x, y, width, height, src) {
|
|
return {
|
|
id: generateId(),
|
|
element_type: "image",
|
|
x: x,
|
|
y: y,
|
|
width: width,
|
|
height: height,
|
|
rotation: 0,
|
|
content: { src: src },
|
|
style: {
|
|
opacity: 1,
|
|
borderRadius: 0,
|
|
},
|
|
animations: [],
|
|
z_index: 1,
|
|
locked: false,
|
|
};
|
|
}
|
|
|
|
function createDefaultTheme() {
|
|
return {
|
|
name: "Default",
|
|
colors: {
|
|
primary: "#3b82f6",
|
|
secondary: "#64748b",
|
|
accent: "#f59e0b",
|
|
background: "#ffffff",
|
|
text: "#1e293b",
|
|
text_light: "#64748b",
|
|
},
|
|
fonts: {
|
|
heading: "Inter",
|
|
body: "Inter",
|
|
},
|
|
};
|
|
}
|
|
|
|
function renderThumbnails() {
|
|
if (!elements.thumbnails) return;
|
|
|
|
elements.thumbnails.innerHTML = state.slides
|
|
.map(
|
|
(slide, index) => `
|
|
<div class="slide-thumbnail ${index === state.currentSlideIndex ? "active" : ""}"
|
|
data-index="${index}"
|
|
onclick="window.slidesApp.goToSlide(${index})"
|
|
oncontextmenu="window.slidesApp.showSlideContextMenu(event, ${index})">
|
|
<div class="slide-thumbnail-preview" id="thumbnail-${index}">
|
|
${renderSlideThumbnailContent(slide)}
|
|
</div>
|
|
<span class="slide-thumbnail-number">${index + 1}</span>
|
|
</div>
|
|
`,
|
|
)
|
|
.join("");
|
|
}
|
|
|
|
function renderSlideThumbnailContent(slide) {
|
|
const scale = 0.15;
|
|
let html = `<div style="transform: scale(${scale}); transform-origin: top left; width: ${CONFIG.CANVAS_WIDTH}px; height: ${CONFIG.CANVAS_HEIGHT}px; background: ${slide.background.color || "#ffffff"}; position: relative;">`;
|
|
|
|
slide.elements.forEach((element) => {
|
|
html += renderElementHTML(element, true);
|
|
});
|
|
|
|
html += "</div>";
|
|
return html;
|
|
}
|
|
|
|
function renderCurrentSlide() {
|
|
if (!elements.canvas) return;
|
|
|
|
const slide = state.slides[state.currentSlideIndex];
|
|
if (!slide) return;
|
|
|
|
elements.canvas.style.background = slide.background.color || "#ffffff";
|
|
elements.canvas.innerHTML = "";
|
|
|
|
slide.elements.forEach((element) => {
|
|
const el = document.createElement("div");
|
|
el.innerHTML = renderElementHTML(element);
|
|
const elementNode = el.firstElementChild;
|
|
if (elementNode) {
|
|
elements.canvas.appendChild(elementNode);
|
|
bindElementEvents(elementNode, element);
|
|
}
|
|
});
|
|
|
|
clearSelection();
|
|
updateSlideCounter();
|
|
}
|
|
|
|
function renderElementHTML(element, isThumbnail = false) {
|
|
const style = buildElementStyle(element);
|
|
const classes = ["slide-element"];
|
|
|
|
if (
|
|
state.selectedElement &&
|
|
state.selectedElement.id === element.id &&
|
|
!isThumbnail
|
|
) {
|
|
classes.push("selected");
|
|
}
|
|
if (element.locked) {
|
|
classes.push("locked");
|
|
}
|
|
|
|
let content = "";
|
|
|
|
switch (element.element_type) {
|
|
case "text":
|
|
classes.push("slide-element-text");
|
|
content = escapeHtml(element.content.text || "").replace(/\n/g, "<br>");
|
|
break;
|
|
case "image":
|
|
classes.push("slide-element-image");
|
|
content = `<img src="${element.content.src}" alt="" draggable="false">`;
|
|
break;
|
|
case "shape":
|
|
classes.push("slide-element-shape");
|
|
content = renderShapeSVG(element);
|
|
break;
|
|
case "chart":
|
|
classes.push("slide-element-chart");
|
|
content = renderChartContent(element);
|
|
break;
|
|
}
|
|
|
|
return `
|
|
<div class="${classes.join(" ")}"
|
|
data-id="${element.id}"
|
|
style="${style}">
|
|
${content}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function buildElementStyle(element) {
|
|
const styles = [
|
|
`left: ${element.x}px`,
|
|
`top: ${element.y}px`,
|
|
`width: ${element.width}px`,
|
|
`height: ${element.height}px`,
|
|
`transform: rotate(${element.rotation || 0}deg)`,
|
|
`z-index: ${element.z_index || 1}`,
|
|
];
|
|
|
|
const s = element.style || {};
|
|
|
|
if (element.element_type === "text") {
|
|
if (s.fontFamily) styles.push(`font-family: ${s.fontFamily}`);
|
|
if (s.fontSize) styles.push(`font-size: ${s.fontSize}px`);
|
|
if (s.fontWeight) styles.push(`font-weight: ${s.fontWeight}`);
|
|
if (s.fontStyle) styles.push(`font-style: ${s.fontStyle}`);
|
|
if (s.textAlign) styles.push(`text-align: ${s.textAlign}`);
|
|
if (s.color) styles.push(`color: ${s.color}`);
|
|
if (s.lineHeight) styles.push(`line-height: ${s.lineHeight}`);
|
|
if (s.fill) styles.push(`background: ${s.fill}`);
|
|
}
|
|
|
|
if (element.element_type === "shape") {
|
|
if (s.opacity) styles.push(`opacity: ${s.opacity}`);
|
|
}
|
|
|
|
return styles.join("; ");
|
|
}
|
|
|
|
function renderShapeSVG(element) {
|
|
const shapeType = element.content.shape_type || "rectangle";
|
|
const fill = element.style.fill || "#3b82f6";
|
|
const stroke = element.style.stroke || "none";
|
|
const strokeWidth = element.style.strokeWidth || 0;
|
|
|
|
let path = "";
|
|
switch (shapeType) {
|
|
case "rectangle":
|
|
path = `<rect x="0" y="0" width="100%" height="100%" rx="${element.style.borderRadius || 0}"/>`;
|
|
break;
|
|
case "rounded-rectangle":
|
|
path = `<rect x="0" y="0" width="100%" height="100%" rx="12"/>`;
|
|
break;
|
|
case "ellipse":
|
|
path = `<ellipse cx="50%" cy="50%" rx="50%" ry="50%"/>`;
|
|
break;
|
|
case "triangle":
|
|
path = `<polygon points="50,0 100,100 0,100"/>`;
|
|
break;
|
|
case "diamond":
|
|
path = `<polygon points="50,0 100,50 50,100 0,50"/>`;
|
|
break;
|
|
case "star":
|
|
path = `<polygon points="50,0 61,35 98,35 68,57 79,91 50,70 21,91 32,57 2,35 39,35"/>`;
|
|
break;
|
|
case "arrow-right":
|
|
path = `<polygon points="0,25 60,25 60,0 100,50 60,100 60,75 0,75"/>`;
|
|
break;
|
|
case "callout":
|
|
path = `<path d="M0,0 L100,0 L100,70 L40,70 L20,100 L20,70 L0,70 Z"/>`;
|
|
break;
|
|
default:
|
|
path = `<rect x="0" y="0" width="100%" height="100%"/>`;
|
|
}
|
|
|
|
return `
|
|
<svg viewBox="0 0 100 100" preserveAspectRatio="none" style="fill: ${fill}; stroke: ${stroke}; stroke-width: ${strokeWidth};">
|
|
${path}
|
|
</svg>
|
|
`;
|
|
}
|
|
|
|
function renderChartContent(element) {
|
|
return '<div style="display:flex;align-items:center;justify-content:center;height:100%;color:#999;">Chart</div>';
|
|
}
|
|
|
|
function bindElementEvents(node, element) {
|
|
node.addEventListener("mousedown", (e) =>
|
|
handleElementMouseDown(e, element),
|
|
);
|
|
node.addEventListener("dblclick", (e) =>
|
|
handleElementDoubleClick(e, element),
|
|
);
|
|
}
|
|
|
|
function handleCanvasMouseDown(e) {
|
|
if (e.target === elements.canvas) {
|
|
clearSelection();
|
|
}
|
|
}
|
|
|
|
function handleCanvasDoubleClick(e) {
|
|
if (e.target === elements.canvas) {
|
|
const rect = elements.canvas.getBoundingClientRect();
|
|
const x = (e.clientX - rect.left) / state.zoom;
|
|
const y = (e.clientY - rect.top) / state.zoom;
|
|
addTextBoxAt(x - 100, y - 25);
|
|
}
|
|
}
|
|
|
|
function addTextBox() {
|
|
const slide = state.slides[state.currentSlideIndex];
|
|
const centerX = CONFIG.CANVAS_WIDTH / 2 - 150;
|
|
const centerY = CONFIG.CANVAS_HEIGHT / 2 - 30;
|
|
addTextBoxAt(centerX, centerY);
|
|
}
|
|
|
|
function addTextBoxAt(x, y) {
|
|
const slide = state.slides[state.currentSlideIndex];
|
|
const textElement = createTextElement(x, y, 300, 60, "Click to edit text", {
|
|
fontSize: 24,
|
|
color: "#1e293b",
|
|
});
|
|
slide.elements.push(textElement);
|
|
saveToHistory();
|
|
renderCurrentSlide();
|
|
selectElement(textElement);
|
|
scheduleAutoSave();
|
|
broadcastChange("elementAdded", { element: textElement });
|
|
}
|
|
|
|
function handleElementMouseDown(e, element) {
|
|
e.stopPropagation();
|
|
|
|
if (element.locked) return;
|
|
|
|
selectElement(element);
|
|
|
|
if (e.button === 0) {
|
|
state.isDragging = true;
|
|
state.dragStart = {
|
|
x: e.clientX,
|
|
y: e.clientY,
|
|
elementX: element.x,
|
|
elementY: element.y,
|
|
};
|
|
}
|
|
}
|
|
|
|
function handleElementDoubleClick(e, element) {
|
|
e.stopPropagation();
|
|
|
|
if (element.element_type === "text") {
|
|
startTextEditing(element);
|
|
}
|
|
}
|
|
|
|
function handleResizeStart(e) {
|
|
e.stopPropagation();
|
|
|
|
if (!state.selectedElement) return;
|
|
|
|
const handle = e.target.dataset.handle;
|
|
if (handle === "rotate") {
|
|
state.isRotating = true;
|
|
} else {
|
|
state.isResizing = true;
|
|
state.resizeHandle = handle;
|
|
}
|
|
|
|
state.dragStart = {
|
|
x: e.clientX,
|
|
y: e.clientY,
|
|
elementX: state.selectedElement.x,
|
|
elementY: state.selectedElement.y,
|
|
elementWidth: state.selectedElement.width,
|
|
elementHeight: state.selectedElement.height,
|
|
elementRotation: state.selectedElement.rotation || 0,
|
|
};
|
|
}
|
|
|
|
function handleMouseMove(e) {
|
|
if (state.isDragging && state.selectedElement && state.dragStart) {
|
|
const dx = (e.clientX - state.dragStart.x) / state.zoom;
|
|
const dy = (e.clientY - state.dragStart.y) / state.zoom;
|
|
|
|
state.selectedElement.x = state.dragStart.elementX + dx;
|
|
state.selectedElement.y = state.dragStart.elementY + dy;
|
|
|
|
updateElementPosition(state.selectedElement);
|
|
updateSelectionHandles();
|
|
broadcastChange("elementMove", state.selectedElement);
|
|
} else if (state.isResizing && state.selectedElement && state.dragStart) {
|
|
const dx = (e.clientX - state.dragStart.x) / state.zoom;
|
|
const dy = (e.clientY - state.dragStart.y) / state.zoom;
|
|
|
|
resizeElement(dx, dy);
|
|
updateElementPosition(state.selectedElement);
|
|
updateSelectionHandles();
|
|
broadcastChange("elementResize", state.selectedElement);
|
|
} else if (state.isRotating && state.selectedElement) {
|
|
const rect = elements.canvas.getBoundingClientRect();
|
|
const centerX = state.selectedElement.x + state.selectedElement.width / 2;
|
|
const centerY =
|
|
state.selectedElement.y + state.selectedElement.height / 2;
|
|
const mouseX = (e.clientX - rect.left) / state.zoom;
|
|
const mouseY = (e.clientY - rect.top) / state.zoom;
|
|
|
|
const angle =
|
|
Math.atan2(mouseY - centerY, mouseX - centerX) * (180 / Math.PI) + 90;
|
|
state.selectedElement.rotation = Math.round(angle);
|
|
|
|
updateElementPosition(state.selectedElement);
|
|
updateSelectionHandles();
|
|
updatePropertiesPanel();
|
|
broadcastChange("elementRotate", state.selectedElement);
|
|
}
|
|
|
|
broadcastCursor(e);
|
|
}
|
|
|
|
function resizeElement(dx, dy) {
|
|
const el = state.selectedElement;
|
|
const s = state.dragStart;
|
|
|
|
switch (state.resizeHandle) {
|
|
case "se":
|
|
el.width = Math.max(CONFIG.MIN_ELEMENT_SIZE, s.elementWidth + dx);
|
|
el.height = Math.max(CONFIG.MIN_ELEMENT_SIZE, s.elementHeight + dy);
|
|
break;
|
|
case "sw":
|
|
el.x = s.elementX + dx;
|
|
el.width = Math.max(CONFIG.MIN_ELEMENT_SIZE, s.elementWidth - dx);
|
|
el.height = Math.max(CONFIG.MIN_ELEMENT_SIZE, s.elementHeight + dy);
|
|
break;
|
|
case "ne":
|
|
el.y = s.elementY + dy;
|
|
el.width = Math.max(CONFIG.MIN_ELEMENT_SIZE, s.elementWidth + dx);
|
|
el.height = Math.max(CONFIG.MIN_ELEMENT_SIZE, s.elementHeight - dy);
|
|
break;
|
|
case "nw":
|
|
el.x = s.elementX + dx;
|
|
el.y = s.elementY + dy;
|
|
el.width = Math.max(CONFIG.MIN_ELEMENT_SIZE, s.elementWidth - dx);
|
|
el.height = Math.max(CONFIG.MIN_ELEMENT_SIZE, s.elementHeight - dy);
|
|
break;
|
|
case "n":
|
|
el.y = s.elementY + dy;
|
|
el.height = Math.max(CONFIG.MIN_ELEMENT_SIZE, s.elementHeight - dy);
|
|
break;
|
|
case "s":
|
|
el.height = Math.max(CONFIG.MIN_ELEMENT_SIZE, s.elementHeight + dy);
|
|
break;
|
|
case "e":
|
|
el.width = Math.max(CONFIG.MIN_ELEMENT_SIZE, s.elementWidth + dx);
|
|
break;
|
|
case "w":
|
|
el.x = s.elementX + dx;
|
|
el.width = Math.max(CONFIG.MIN_ELEMENT_SIZE, s.elementWidth - dx);
|
|
break;
|
|
}
|
|
}
|
|
|
|
function handleMouseUp() {
|
|
if (state.isDragging || state.isResizing || state.isRotating) {
|
|
saveToHistory();
|
|
scheduleAutoSave();
|
|
}
|
|
|
|
state.isDragging = false;
|
|
state.isResizing = false;
|
|
state.isRotating = false;
|
|
state.dragStart = null;
|
|
state.resizeHandle = null;
|
|
}
|
|
|
|
function handleKeyDown(e) {
|
|
if (
|
|
e.target.tagName === "INPUT" ||
|
|
e.target.tagName === "TEXTAREA" ||
|
|
e.target.isContentEditable
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const isMod = e.ctrlKey || e.metaKey;
|
|
|
|
if (isMod && e.key === "z") {
|
|
e.preventDefault();
|
|
if (e.shiftKey) {
|
|
redo();
|
|
} else {
|
|
undo();
|
|
}
|
|
} else if (isMod && e.key === "y") {
|
|
e.preventDefault();
|
|
redo();
|
|
} else if (isMod && e.key === "c") {
|
|
e.preventDefault();
|
|
copyElement();
|
|
} else if (isMod && e.key === "x") {
|
|
e.preventDefault();
|
|
cutElement();
|
|
} else if (isMod && e.key === "v") {
|
|
e.preventDefault();
|
|
pasteElement();
|
|
} else if (isMod && e.key === "d") {
|
|
e.preventDefault();
|
|
duplicateElement();
|
|
} else if (isMod && e.key === "s") {
|
|
e.preventDefault();
|
|
savePresentation();
|
|
} else if (isMod && e.key === "a") {
|
|
e.preventDefault();
|
|
selectAll();
|
|
} else if (e.key === "Delete" || e.key === "Backspace") {
|
|
if (state.selectedElement) {
|
|
e.preventDefault();
|
|
deleteElement();
|
|
}
|
|
} else if (e.key === "Escape") {
|
|
clearSelection();
|
|
hideContextMenus();
|
|
if (state.isPresenting) {
|
|
exitPresentation();
|
|
}
|
|
} else if (e.key === "ArrowUp" && state.selectedElement) {
|
|
e.preventDefault();
|
|
state.selectedElement.y -= e.shiftKey ? 10 : 1;
|
|
updateElementPosition(state.selectedElement);
|
|
updateSelectionHandles();
|
|
} else if (e.key === "ArrowDown" && state.selectedElement) {
|
|
e.preventDefault();
|
|
state.selectedElement.y += e.shiftKey ? 10 : 1;
|
|
updateElementPosition(state.selectedElement);
|
|
updateSelectionHandles();
|
|
} else if (e.key === "ArrowLeft" && state.selectedElement) {
|
|
e.preventDefault();
|
|
state.selectedElement.x -= e.shiftKey ? 10 : 1;
|
|
updateElementPosition(state.selectedElement);
|
|
updateSelectionHandles();
|
|
} else if (e.key === "ArrowRight" && state.selectedElement) {
|
|
e.preventDefault();
|
|
state.selectedElement.x += e.shiftKey ? 10 : 1;
|
|
updateElementPosition(state.selectedElement);
|
|
updateSelectionHandles();
|
|
} else if (e.key === "F5") {
|
|
e.preventDefault();
|
|
startPresentation();
|
|
} else if (
|
|
e.key === "PageDown" ||
|
|
(e.key === "ArrowRight" && !state.selectedElement)
|
|
) {
|
|
e.preventDefault();
|
|
goToSlide(state.currentSlideIndex + 1);
|
|
} else if (
|
|
e.key === "PageUp" ||
|
|
(e.key === "ArrowLeft" && !state.selectedElement)
|
|
) {
|
|
e.preventDefault();
|
|
goToSlide(state.currentSlideIndex - 1);
|
|
}
|
|
}
|
|
|
|
function selectElement(element) {
|
|
state.selectedElement = element;
|
|
|
|
document.querySelectorAll(".slide-element.selected").forEach((el) => {
|
|
el.classList.remove("selected");
|
|
});
|
|
|
|
const node = document.querySelector(`[data-id="${element.id}"]`);
|
|
if (node) {
|
|
node.classList.add("selected");
|
|
}
|
|
|
|
updateSelectionHandles();
|
|
updatePropertiesPanel();
|
|
showPropertiesPanel();
|
|
}
|
|
|
|
function clearSelection() {
|
|
state.selectedElement = null;
|
|
|
|
document.querySelectorAll(".slide-element.selected").forEach((el) => {
|
|
el.classList.remove("selected");
|
|
});
|
|
|
|
hideSelectionHandles();
|
|
updatePropertiesPanel();
|
|
}
|
|
|
|
function updateSelectionHandles() {
|
|
if (!state.selectedElement || !elements.selectionHandles) {
|
|
hideSelectionHandles();
|
|
return;
|
|
}
|
|
|
|
const el = state.selectedElement;
|
|
elements.selectionHandles.classList.remove("hidden");
|
|
elements.selectionHandles.style.left = `${el.x}px`;
|
|
elements.selectionHandles.style.top = `${el.y}px`;
|
|
elements.selectionHandles.style.width = `${el.width}px`;
|
|
elements.selectionHandles.style.height = `${el.height}px`;
|
|
elements.selectionHandles.style.transform = `rotate(${el.rotation || 0}deg)`;
|
|
}
|
|
|
|
function hideSelectionHandles() {
|
|
if (elements.selectionHandles) {
|
|
elements.selectionHandles.classList.add("hidden");
|
|
}
|
|
}
|
|
|
|
function updateElementPosition(element) {
|
|
const node = document.querySelector(`[data-id="${element.id}"]`);
|
|
if (node) {
|
|
node.style.left = `${element.x}px`;
|
|
node.style.top = `${element.y}px`;
|
|
node.style.width = `${element.width}px`;
|
|
node.style.height = `${element.height}px`;
|
|
node.style.transform = `rotate(${element.rotation || 0}deg)`;
|
|
}
|
|
state.isDirty = true;
|
|
}
|
|
|
|
function updatePropertiesPanel() {
|
|
if (!state.selectedElement) {
|
|
document.getElementById("prop-x").value = "";
|
|
document.getElementById("prop-y").value = "";
|
|
document.getElementById("prop-width").value = "";
|
|
document.getElementById("prop-height").value = "";
|
|
document.getElementById("prop-rotation").value = 0;
|
|
document.getElementById("rotation-value").textContent = "0°";
|
|
document.getElementById("prop-opacity").value = 100;
|
|
document.getElementById("opacity-value").textContent = "100%";
|
|
return;
|
|
}
|
|
|
|
const el = state.selectedElement;
|
|
document.getElementById("prop-x").value = Math.round(el.x);
|
|
document.getElementById("prop-y").value = Math.round(el.y);
|
|
document.getElementById("prop-width").value = Math.round(el.width);
|
|
document.getElementById("prop-height").value = Math.round(el.height);
|
|
document.getElementById("prop-rotation").value = el.rotation || 0;
|
|
document.getElementById("rotation-value").textContent =
|
|
`${el.rotation || 0}°`;
|
|
|
|
const opacity = (el.style.opacity || 1) * 100;
|
|
document.getElementById("prop-opacity").value = opacity;
|
|
document.getElementById("opacity-value").textContent =
|
|
`${Math.round(opacity)}%`;
|
|
}
|
|
|
|
function showPropertiesPanel() {
|
|
if (elements.propertiesPanel) {
|
|
elements.propertiesPanel.classList.remove("collapsed");
|
|
}
|
|
}
|
|
|
|
function startTextEditing(element) {
|
|
const node = document.querySelector(`[data-id="${element.id}"]`);
|
|
if (!node) return;
|
|
|
|
node.contentEditable = true;
|
|
node.classList.add("editing");
|
|
node.focus();
|
|
|
|
const range = document.createRange();
|
|
range.selectNodeContents(node);
|
|
const sel = window.getSelection();
|
|
sel.removeAllRanges();
|
|
sel.addRange(range);
|
|
|
|
node.addEventListener(
|
|
"blur",
|
|
() => {
|
|
node.contentEditable = false;
|
|
node.classList.remove("editing");
|
|
element.content.text = node.innerText;
|
|
saveToHistory();
|
|
scheduleAutoSave();
|
|
renderThumbnails();
|
|
},
|
|
{ once: true },
|
|
);
|
|
}
|
|
|
|
function goToSlide(index) {
|
|
if (index < 0 || index >= state.slides.length) return;
|
|
|
|
state.currentSlideIndex = index;
|
|
renderCurrentSlide();
|
|
renderThumbnails();
|
|
updateSlideCounter();
|
|
broadcastChange("slideChange", { slideIndex: index });
|
|
}
|
|
|
|
function addSlide(layout = "title-content") {
|
|
const newSlide = createSlide(layout);
|
|
state.slides.splice(state.currentSlideIndex + 1, 0, newSlide);
|
|
state.currentSlideIndex++;
|
|
saveToHistory();
|
|
renderThumbnails();
|
|
renderCurrentSlide();
|
|
updateSlideCounter();
|
|
scheduleAutoSave();
|
|
broadcastChange("slideAdded", { slideIndex: state.currentSlideIndex });
|
|
}
|
|
|
|
function duplicateSlide() {
|
|
const currentSlide = state.slides[state.currentSlideIndex];
|
|
const duplicated = JSON.parse(JSON.stringify(currentSlide));
|
|
duplicated.id = generateId();
|
|
duplicated.elements.forEach((el) => {
|
|
el.id = generateId();
|
|
});
|
|
state.slides.splice(state.currentSlideIndex + 1, 0, duplicated);
|
|
state.currentSlideIndex++;
|
|
saveToHistory();
|
|
renderThumbnails();
|
|
renderCurrentSlide();
|
|
updateSlideCounter();
|
|
scheduleAutoSave();
|
|
}
|
|
|
|
function deleteSlide() {
|
|
if (state.slides.length <= 1) return;
|
|
|
|
state.slides.splice(state.currentSlideIndex, 1);
|
|
if (state.currentSlideIndex >= state.slides.length) {
|
|
state.currentSlideIndex = state.slides.length - 1;
|
|
}
|
|
saveToHistory();
|
|
renderThumbnails();
|
|
renderCurrentSlide();
|
|
updateSlideCounter();
|
|
scheduleAutoSave();
|
|
broadcastChange("slideDeleted", { slideIndex: state.currentSlideIndex });
|
|
}
|
|
|
|
function updateSlideCounter() {
|
|
const currentEl = document.getElementById("current-slide-num");
|
|
const totalEl = document.getElementById("total-slides-num");
|
|
if (currentEl) currentEl.textContent = state.currentSlideIndex + 1;
|
|
if (totalEl) totalEl.textContent = state.slides.length;
|
|
}
|
|
|
|
function showImageModal() {
|
|
const url = prompt("Enter image URL:");
|
|
if (url) {
|
|
addImage(url);
|
|
}
|
|
}
|
|
|
|
function addImage(url) {
|
|
const slide = state.slides[state.currentSlideIndex];
|
|
const imageElement = createImageElement(100, 100, 400, 300, url);
|
|
slide.elements.push(imageElement);
|
|
saveToHistory();
|
|
renderCurrentSlide();
|
|
selectElement(imageElement);
|
|
scheduleAutoSave();
|
|
}
|
|
|
|
function showShapeModal() {
|
|
addShape("rectangle");
|
|
}
|
|
|
|
function addShape(shapeType) {
|
|
const slide = state.slides[state.currentSlideIndex];
|
|
const shapeElement = createShapeElement(100, 100, 200, 150, shapeType, {
|
|
fill: "#3b82f6",
|
|
});
|
|
slide.elements.push(shapeElement);
|
|
saveToHistory();
|
|
renderCurrentSlide();
|
|
selectElement(shapeElement);
|
|
scheduleAutoSave();
|
|
}
|
|
|
|
function showChartModal() {
|
|
alert("Chart insertion coming soon!");
|
|
}
|
|
|
|
function addTable() {
|
|
alert("Table insertion coming soon!");
|
|
}
|
|
|
|
function setFontFamily(family) {
|
|
if (
|
|
state.selectedElement &&
|
|
state.selectedElement.element_type === "text"
|
|
) {
|
|
state.selectedElement.style.fontFamily = family;
|
|
renderCurrentSlide();
|
|
scheduleAutoSave();
|
|
}
|
|
}
|
|
|
|
function setFontSize(size) {
|
|
if (
|
|
state.selectedElement &&
|
|
state.selectedElement.element_type === "text"
|
|
) {
|
|
state.selectedElement.style.fontSize = parseInt(size, 10);
|
|
renderCurrentSlide();
|
|
scheduleAutoSave();
|
|
}
|
|
}
|
|
|
|
function toggleBold() {
|
|
if (
|
|
state.selectedElement &&
|
|
state.selectedElement.element_type === "text"
|
|
) {
|
|
state.selectedElement.style.fontWeight =
|
|
state.selectedElement.style.fontWeight === "bold" ? "normal" : "bold";
|
|
renderCurrentSlide();
|
|
scheduleAutoSave();
|
|
}
|
|
}
|
|
|
|
function toggleItalic() {
|
|
if (
|
|
state.selectedElement &&
|
|
state.selectedElement.element_type === "text"
|
|
) {
|
|
state.selectedElement.style.fontStyle =
|
|
state.selectedElement.style.fontStyle === "italic"
|
|
? "normal"
|
|
: "italic";
|
|
renderCurrentSlide();
|
|
scheduleAutoSave();
|
|
}
|
|
}
|
|
|
|
function toggleUnderline() {
|
|
if (
|
|
state.selectedElement &&
|
|
state.selectedElement.element_type === "text"
|
|
) {
|
|
state.selectedElement.style.textDecoration =
|
|
state.selectedElement.style.textDecoration === "underline"
|
|
? "none"
|
|
: "underline";
|
|
renderCurrentSlide();
|
|
scheduleAutoSave();
|
|
}
|
|
}
|
|
|
|
function toggleSidebar() {
|
|
if (elements.sidebar) {
|
|
elements.sidebar.classList.toggle("open");
|
|
}
|
|
}
|
|
|
|
function renamePresentation(name) {
|
|
state.presentationName = name;
|
|
scheduleAutoSave();
|
|
}
|
|
|
|
function searchPresentations(query) {
|
|
console.log("Searching:", query);
|
|
}
|
|
|
|
function startPresentation() {
|
|
state.isPresenting = true;
|
|
document.body.classList.add("presenting");
|
|
renderCurrentSlide();
|
|
}
|
|
|
|
function exitPresentation() {
|
|
state.isPresenting = false;
|
|
document.body.classList.remove("presenting");
|
|
renderCurrentSlide();
|
|
}
|
|
|
|
// Initialize when DOM is ready
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", init);
|
|
} else {
|
|
init();
|
|
}
|
|
|
|
// Expose functions globally
|
|
window.addSlide = addSlide;
|
|
window.addTextBox = addTextBox;
|
|
window.showImageModal = showImageModal;
|
|
window.showShapeModal = showShapeModal;
|
|
window.showChartModal = showChartModal;
|
|
window.addTable = addTable;
|
|
window.setFontFamily = setFontFamily;
|
|
window.setFontSize = setFontSize;
|
|
window.toggleBold = toggleBold;
|
|
window.toggleItalic = toggleItalic;
|
|
window.toggleUnderline = toggleUnderline;
|
|
window.toggleSidebar = toggleSidebar;
|
|
window.renamePresentation = renamePresentation;
|
|
window.searchPresentations = searchPresentations;
|
|
window.duplicateSlide = duplicateSlide;
|
|
window.deleteSlide = deleteSlide;
|
|
window.startPresentation = startPresentation;
|
|
window.exitPresentation = exitPresentation;
|
|
})();
|