786 lines
24 KiB
HTML
786 lines
24 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Video Timeline Editor</title>
|
|
<style>
|
|
:root {
|
|
--bg-body: #121212;
|
|
--bg-node: #1e1e1e;
|
|
--bg-overlay: #252525;
|
|
--text-main: #e0e0e0;
|
|
--text-dim: #a0a0a0;
|
|
--timeline-track: #333;
|
|
--timeline-filled: #444;
|
|
--accent-blue: #3b82f6;
|
|
--accent-orange: #f97316;
|
|
--border-color: #404040;
|
|
--input-bg: #2d2d2d;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
background-color: var(--bg-body);
|
|
color: var(--text-main);
|
|
margin: 0;
|
|
padding: 20px;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: flex-start;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
/* --- Node Container (The "Small" View) --- */
|
|
.node-container {
|
|
background: var(--bg-node);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 8px;
|
|
width: 400px;
|
|
padding: 16px;
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
.node-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.node-title {
|
|
font-size: 1.1rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.control-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-bottom: 12px;
|
|
font-size: 0.9rem;
|
|
color: var(--text-dim);
|
|
}
|
|
|
|
input[type="number"] {
|
|
background: var(--input-bg);
|
|
border: 1px solid var(--border-color);
|
|
color: var(--text-main);
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
width: 60px;
|
|
}
|
|
|
|
/* --- Timeline Base Styles --- */
|
|
.timeline-scroll-container {
|
|
width: 100%;
|
|
overflow-x: auto;
|
|
background: var(--timeline-track);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 4px;
|
|
position: relative;
|
|
margin-bottom: 12px;
|
|
height: 60px;
|
|
|
|
/* Custom Scrollbar */
|
|
scrollbar-width: thin;
|
|
scrollbar-color: #555 #333;
|
|
}
|
|
|
|
.timeline-scroll-container::-webkit-scrollbar {
|
|
height: 6px;
|
|
}
|
|
|
|
.timeline-scroll-container::-webkit-scrollbar-track {
|
|
background: #333;
|
|
}
|
|
|
|
.timeline-scroll-container::-webkit-scrollbar-thumb {
|
|
background: #555;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.timeline-track {
|
|
height: 100%;
|
|
position: relative;
|
|
min-width: 100%;
|
|
/* Ensures it fills container at minimum */
|
|
cursor: crosshair;
|
|
}
|
|
|
|
/* Grid lines for the track */
|
|
.timeline-grid {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
pointer-events: none;
|
|
background-image: linear-gradient(90deg, #444 1px, transparent 1px);
|
|
background-size: 50px 100%;
|
|
/* Arbitrary grid spacing */
|
|
opacity: 0.3;
|
|
}
|
|
|
|
.marker {
|
|
position: absolute;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 2px;
|
|
/* Hit area width handled by pseudo element usually, but here simple */
|
|
background-color: var(--accent-blue);
|
|
cursor: ew-resize;
|
|
z-index: 10;
|
|
transition: background-color 0.2s;
|
|
}
|
|
|
|
/* Hit area expansion */
|
|
.marker::after {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
bottom: 0;
|
|
left: -4px;
|
|
right: -4px;
|
|
cursor: ew-resize;
|
|
}
|
|
|
|
.marker.orange {
|
|
background-color: var(--accent-orange);
|
|
border-bottom: 4px solid #fff;
|
|
/* Visual indicator */
|
|
}
|
|
|
|
.marker:hover {
|
|
box-shadow: 0 0 4px rgba(255, 255, 255, 0.5);
|
|
}
|
|
|
|
.marker.selected {
|
|
background-color: #fff;
|
|
z-index: 20;
|
|
box-shadow: 0 0 8px rgba(255, 255, 255, 0.8);
|
|
}
|
|
|
|
.marker-label {
|
|
position: absolute;
|
|
bottom: 2px;
|
|
left: 4px;
|
|
font-size: 10px;
|
|
color: #fff;
|
|
pointer-events: none;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
padding: 1px 3px;
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.btn-primary {
|
|
background-color: #404040;
|
|
color: white;
|
|
border: 1px solid #555;
|
|
padding: 6px 12px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
width: 100%;
|
|
font-size: 0.9rem;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
background-color: #505050;
|
|
}
|
|
|
|
/* --- Overlay Editor --- */
|
|
.overlay-backdrop {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
z-index: 100;
|
|
justify-content: center;
|
|
align-items: center;
|
|
backdrop-filter: blur(2px);
|
|
}
|
|
|
|
.editor-modal {
|
|
background: var(--bg-overlay);
|
|
width: 90%;
|
|
max-width: 900px;
|
|
height: 80vh;
|
|
border-radius: 8px;
|
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5);
|
|
border: 1px solid var(--border-color);
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.modal-header {
|
|
padding: 16px;
|
|
border-bottom: 1px solid var(--border-color);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.modal-title {
|
|
margin: 0;
|
|
font-size: 1.2rem;
|
|
}
|
|
|
|
.modal-close {
|
|
background: none;
|
|
border: none;
|
|
color: var(--text-dim);
|
|
font-size: 1.5rem;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.modal-close:hover {
|
|
color: var(--text-main);
|
|
}
|
|
|
|
.modal-body {
|
|
flex: 1;
|
|
padding: 20px;
|
|
overflow-y: auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
}
|
|
|
|
.instructions {
|
|
font-size: 0.85rem;
|
|
color: var(--text-dim);
|
|
background: #333;
|
|
padding: 8px;
|
|
border-radius: 4px;
|
|
border-left: 3px solid var(--accent-blue);
|
|
}
|
|
|
|
/* --- Editor Specific Components --- */
|
|
.editor-timeline-area {
|
|
height: 120px;
|
|
/* Taller timeline in editor */
|
|
}
|
|
|
|
.properties-panel {
|
|
background: #1a1a1a;
|
|
border: 1px solid var(--border-color);
|
|
padding: 16px;
|
|
border-radius: 6px;
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
opacity: 0.5;
|
|
pointer-events: none;
|
|
transition: opacity 0.2s;
|
|
}
|
|
|
|
.properties-panel.active {
|
|
opacity: 1;
|
|
pointer-events: all;
|
|
}
|
|
|
|
.image-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 20px;
|
|
}
|
|
|
|
.image-uploader {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
.uploader-label {
|
|
font-size: 0.8rem;
|
|
color: var(--text-dim);
|
|
}
|
|
|
|
.upload-box {
|
|
width: 100px;
|
|
height: 100px;
|
|
border: 2px dashed #555;
|
|
border-radius: 6px;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
cursor: pointer;
|
|
position: relative;
|
|
overflow: hidden;
|
|
background: #222;
|
|
transition: border-color 0.2s;
|
|
}
|
|
|
|
.upload-box:hover {
|
|
border-color: #777;
|
|
}
|
|
|
|
.upload-box span {
|
|
font-size: 2rem;
|
|
color: #555;
|
|
}
|
|
|
|
.uploaded-img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
}
|
|
|
|
.transition-toggle {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
cursor: pointer;
|
|
color: #555;
|
|
}
|
|
|
|
.transition-toggle.active {
|
|
color: var(--accent-blue);
|
|
}
|
|
|
|
.transition-icon {
|
|
font-size: 1.5rem;
|
|
border: 1px solid transparent;
|
|
padding: 4px;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.transition-toggle.active .transition-icon {
|
|
border-color: var(--accent-blue);
|
|
}
|
|
|
|
textarea {
|
|
background: var(--input-bg);
|
|
border: 1px solid var(--border-color);
|
|
color: var(--text-main);
|
|
padding: 12px;
|
|
border-radius: 4px;
|
|
resize: none;
|
|
height: 80px;
|
|
font-family: inherit;
|
|
}
|
|
|
|
textarea:focus {
|
|
outline: 1px solid var(--accent-blue);
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<!-- --- Node View --- -->
|
|
<div class="node-container">
|
|
<div class="node-header">
|
|
<span class="node-title">Video Timeline</span>
|
|
</div>
|
|
|
|
<div class="control-row">
|
|
<label>Duration (s):</label>
|
|
<input type="number" id="durationInput" value="30" min="5">
|
|
</div>
|
|
|
|
<!-- Mini Timeline Preview -->
|
|
<div class="timeline-scroll-container" style="height: 40px; overflow:hidden;">
|
|
<div id="miniTrack" class="timeline-track">
|
|
<!-- Markers go here -->
|
|
</div>
|
|
</div>
|
|
|
|
<button class="btn-primary" id="openEditorBtn">Open Editor</button>
|
|
</div>
|
|
|
|
|
|
<!-- --- Overlay Editor --- -->
|
|
<div class="overlay-backdrop" id="editorOverlay">
|
|
<div class="editor-modal">
|
|
<div class="modal-header">
|
|
<h2 class="modal-title">Timeline Editor</h2>
|
|
<button class="modal-close" id="closeEditorBtn">×</button>
|
|
</div>
|
|
|
|
<div class="modal-body">
|
|
<div class="instructions">
|
|
<strong>Controls:</strong> Click to add marker. <strong>Alt+Click</strong> for orange frame. Drag to
|
|
move. <strong>Ctrl+Click</strong> to remove.
|
|
</div>
|
|
|
|
<!-- Main Editor Timeline -->
|
|
<div class="timeline-scroll-container editor-timeline-area" id="mainScrollContainer">
|
|
<div id="mainTrack" class="timeline-track">
|
|
<div class="timeline-grid"></div>
|
|
<!-- Markers go here -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Selected Marker Properties -->
|
|
<div class="properties-panel" id="propertiesPanel">
|
|
<div class="image-row">
|
|
<!-- Start Image (or Frame Image) -->
|
|
<div class="image-uploader">
|
|
<span class="uploader-label" id="labelImg1">Start Image</span>
|
|
<div class="upload-box" id="boxImg1">
|
|
<span>+</span>
|
|
</div>
|
|
<input type="file" id="fileImg1" hidden accept="image/*">
|
|
</div>
|
|
|
|
<!-- Transition Control -->
|
|
<div class="transition-toggle" id="cutTransition">
|
|
<div class="transition-icon">✂️</div>
|
|
<span style="font-size:0.7rem; margin-top:4px;">CUT</span>
|
|
</div>
|
|
|
|
<!-- End Image (Hidden for orange markers) -->
|
|
<div class="image-uploader" id="wrapperImg2">
|
|
<span class="uploader-label">End Image</span>
|
|
<div class="upload-box" id="boxImg2">
|
|
<span>+</span>
|
|
</div>
|
|
<input type="file" id="fileImg2" hidden accept="image/*">
|
|
</div>
|
|
</div>
|
|
|
|
<div style="display:flex; flex-direction:column; gap:6px;">
|
|
<label style="font-size:0.9rem; color:var(--text-dim);">Prompt / Notes</label>
|
|
<textarea id="promptInput" placeholder="Enter prompt or details for this segment..."></textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// --- State Management ---
|
|
const state = {
|
|
duration: 30, // seconds
|
|
markers: [],
|
|
selectedId: null,
|
|
pixelsPerSec: 20 // Zoom level in editor
|
|
};
|
|
|
|
// --- DOM Elements ---
|
|
const durationInput = document.getElementById('durationInput');
|
|
const openEditorBtn = document.getElementById('openEditorBtn');
|
|
const closeEditorBtn = document.getElementById('closeEditorBtn');
|
|
const editorOverlay = document.getElementById('editorOverlay');
|
|
|
|
const mainScrollContainer = document.getElementById('mainScrollContainer');
|
|
const mainTrack = document.getElementById('mainTrack');
|
|
const miniTrack = document.getElementById('miniTrack');
|
|
|
|
const propertiesPanel = document.getElementById('propertiesPanel');
|
|
const labelImg1 = document.getElementById('labelImg1');
|
|
const wrapperImg2 = document.getElementById('wrapperImg2');
|
|
const cutTransition = document.getElementById('cutTransition');
|
|
const promptInput = document.getElementById('promptInput');
|
|
|
|
const uploaders = [
|
|
{ box: document.getElementById('boxImg1'), input: document.getElementById('fileImg1'), key: 'img1' },
|
|
{ box: document.getElementById('boxImg2'), input: document.getElementById('fileImg2'), key: 'img2' }
|
|
];
|
|
|
|
// --- Initialization ---
|
|
function init() {
|
|
durationInput.addEventListener('change', (e) => updateDuration(e.target.value));
|
|
|
|
openEditorBtn.addEventListener('click', () => {
|
|
editorOverlay.style.display = 'flex';
|
|
render();
|
|
});
|
|
|
|
closeEditorBtn.addEventListener('click', () => {
|
|
editorOverlay.style.display = 'none';
|
|
render(); // Re-render mini view
|
|
});
|
|
|
|
// Track Interactions
|
|
mainTrack.addEventListener('mousedown', handleTrackClick);
|
|
document.addEventListener('mousemove', handleDrag);
|
|
document.addEventListener('mouseup', endDrag);
|
|
|
|
// Property Inputs
|
|
promptInput.addEventListener('input', (e) => updateMarkerData('prompt', e.target.value));
|
|
cutTransition.addEventListener('click', () => toggleTransition());
|
|
|
|
// Image Uploaders
|
|
uploaders.forEach((u, index) => setupUploader(u, index));
|
|
|
|
// Initial Render
|
|
render();
|
|
}
|
|
|
|
// --- Core Logic ---
|
|
|
|
function updateDuration(val) {
|
|
let v = parseFloat(val);
|
|
if (v < 1) v = 1;
|
|
state.duration = v;
|
|
durationInput.value = v;
|
|
|
|
// Remove markers out of bounds
|
|
state.markers = state.markers.filter(m => m.time <= state.duration);
|
|
render();
|
|
}
|
|
|
|
function createMarker(time, type) {
|
|
return {
|
|
id: 'm_' + Date.now() + Math.random().toString(36).substr(2, 5),
|
|
time: time,
|
|
type: type, // 'standard' or 'orange'
|
|
prompt: '',
|
|
img1: null,
|
|
img2: null,
|
|
cut: false
|
|
};
|
|
}
|
|
|
|
// --- Interaction Handlers ---
|
|
|
|
function handleTrackClick(e) {
|
|
if (e.target.closest('.marker')) return; // handled by marker mousedown
|
|
|
|
const rect = mainTrack.getBoundingClientRect();
|
|
const x = e.clientX - rect.left;
|
|
const time = (x / rect.width) * state.duration;
|
|
|
|
if (time < 0 || time > state.duration) return;
|
|
|
|
const type = e.altKey ? 'orange' : 'standard';
|
|
const newMarker = createMarker(time, type);
|
|
|
|
state.markers.push(newMarker);
|
|
selectMarker(newMarker.id);
|
|
render();
|
|
}
|
|
|
|
let dragTargetId = null;
|
|
|
|
function startDrag(e, id) {
|
|
e.stopPropagation(); // prevent track creation
|
|
|
|
if (e.ctrlKey) {
|
|
// Delete
|
|
state.markers = state.markers.filter(m => m.id !== id);
|
|
if (state.selectedId === id) selectMarker(null);
|
|
render();
|
|
return;
|
|
}
|
|
|
|
selectMarker(id);
|
|
dragTargetId = id;
|
|
mainTrack.style.cursor = 'ew-resize';
|
|
}
|
|
|
|
function handleDrag(e) {
|
|
if (!dragTargetId) return;
|
|
|
|
const rect = mainTrack.getBoundingClientRect();
|
|
let x = e.clientX - rect.left;
|
|
|
|
// Clamp visual bounds
|
|
if (x < 0) x = 0;
|
|
if (x > rect.width) x = rect.width;
|
|
|
|
const newTime = (x / rect.width) * state.duration;
|
|
|
|
const m = state.markers.find(m => m.id === dragTargetId);
|
|
if (m) {
|
|
m.time = newTime;
|
|
render(); // This is efficient enough for this scale
|
|
}
|
|
}
|
|
|
|
function endDrag() {
|
|
dragTargetId = null;
|
|
mainTrack.style.cursor = 'crosshair';
|
|
}
|
|
|
|
function selectMarker(id) {
|
|
state.selectedId = id;
|
|
renderProperties();
|
|
render(); // to update visual selection class
|
|
}
|
|
|
|
function updateMarkerData(key, value) {
|
|
if (!state.selectedId) return;
|
|
const m = state.markers.find(m => m.id === state.selectedId);
|
|
if (m) m[key] = value;
|
|
}
|
|
|
|
function toggleTransition() {
|
|
if (!state.selectedId) return;
|
|
const m = state.markers.find(m => m.id === state.selectedId);
|
|
if (m) {
|
|
m.cut = !m.cut;
|
|
renderProperties();
|
|
}
|
|
}
|
|
|
|
// --- Rendering ---
|
|
|
|
function render() {
|
|
// 1. Calculate Widths
|
|
// Editor: fixed pixels per second
|
|
const editorWidth = state.duration * state.pixelsPerSec;
|
|
mainTrack.style.width = `${editorWidth}px`;
|
|
|
|
// Mini: 100% width
|
|
miniTrack.style.width = '100%';
|
|
|
|
// 2. Clear
|
|
mainTrack.innerHTML = '<div class="timeline-grid"></div>'; // keep grid
|
|
miniTrack.innerHTML = '';
|
|
|
|
// 3. Render Markers
|
|
state.markers.forEach(m => {
|
|
// Editor Marker
|
|
const elMain = createMarkerElement(m, true);
|
|
mainTrack.appendChild(elMain);
|
|
|
|
// Mini Marker
|
|
const elMini = createMarkerElement(m, false);
|
|
miniTrack.appendChild(elMini);
|
|
});
|
|
}
|
|
|
|
function createMarkerElement(m, isEditor) {
|
|
const el = document.createElement('div');
|
|
el.className = `marker ${m.type}`;
|
|
if (isEditor && m.id === state.selectedId) el.classList.add('selected');
|
|
|
|
// Position percentage
|
|
const pct = (m.time / state.duration) * 100;
|
|
el.style.left = `${pct}%`;
|
|
|
|
if (isEditor) {
|
|
// Add timestamp label
|
|
const label = document.createElement('div');
|
|
label.className = 'marker-label';
|
|
label.innerText = m.time.toFixed(1) + 's';
|
|
el.appendChild(label);
|
|
|
|
// Event
|
|
el.addEventListener('mousedown', (e) => startDrag(e, m.id));
|
|
}
|
|
|
|
return el;
|
|
}
|
|
|
|
function renderProperties() {
|
|
if (!state.selectedId) {
|
|
propertiesPanel.classList.remove('active');
|
|
return;
|
|
}
|
|
|
|
const m = state.markers.find(m => m.id === state.selectedId);
|
|
if (!m) return; // Should not happen
|
|
|
|
propertiesPanel.classList.add('active');
|
|
promptInput.value = m.prompt;
|
|
|
|
// Type specific UI
|
|
if (m.type === 'orange') {
|
|
labelImg1.innerText = "Frame Image";
|
|
wrapperImg2.style.display = 'none';
|
|
} else {
|
|
labelImg1.innerText = "Start Image";
|
|
wrapperImg2.style.display = 'flex';
|
|
}
|
|
|
|
// Transition State
|
|
if (m.cut) cutTransition.classList.add('active');
|
|
else cutTransition.classList.remove('active');
|
|
|
|
// Images
|
|
updateUploaderBox(0, m.img1);
|
|
updateUploaderBox(1, m.img2);
|
|
}
|
|
|
|
// --- Image Handling ---
|
|
|
|
function setupUploader(u, index) {
|
|
// Click box to trigger file or delete
|
|
u.box.addEventListener('click', (e) => {
|
|
const m = state.markers.find(m => m.id === state.selectedId);
|
|
if (!m) return;
|
|
|
|
if (m[u.key]) {
|
|
// Remove existing
|
|
m[u.key] = null;
|
|
renderProperties();
|
|
} else {
|
|
u.input.click();
|
|
}
|
|
});
|
|
|
|
// File Change
|
|
u.input.addEventListener('change', (e) => {
|
|
if (e.target.files && e.target.files[0]) {
|
|
handleFileUpload(e.target.files[0], u.key);
|
|
e.target.value = ''; // reset
|
|
}
|
|
});
|
|
|
|
// Drag Drop
|
|
u.box.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
u.box.style.borderColor = '#fff';
|
|
});
|
|
u.box.addEventListener('dragleave', (e) => {
|
|
e.preventDefault();
|
|
u.box.style.borderColor = '';
|
|
});
|
|
u.box.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
u.box.style.borderColor = '';
|
|
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
|
|
handleFileUpload(e.dataTransfer.files[0], u.key);
|
|
}
|
|
});
|
|
}
|
|
|
|
function handleFileUpload(file, key) {
|
|
if (!state.selectedId) return;
|
|
// Mock Upload to ComfyUI (Local URL)
|
|
const url = URL.createObjectURL(file);
|
|
const m = state.markers.find(m => m.id === state.selectedId);
|
|
if (m) {
|
|
m[key] = url;
|
|
renderProperties();
|
|
}
|
|
}
|
|
|
|
function updateUploaderBox(index, url) {
|
|
const u = uploaders[index];
|
|
u.box.innerHTML = '';
|
|
|
|
if (url) {
|
|
const img = document.createElement('img');
|
|
img.src = url;
|
|
img.className = 'uploaded-img';
|
|
u.box.appendChild(img);
|
|
} else {
|
|
const span = document.createElement('span');
|
|
span.innerText = '+';
|
|
u.box.appendChild(span);
|
|
}
|
|
}
|
|
|
|
// Start
|
|
init();
|
|
|
|
</script>
|
|
</body>
|
|
|
|
</html> |