1
0
Fork 0
mirror of https://github.com/codex-team/codex.docs.git synced 2025-07-21 06:09:41 +02:00
codex.docs/src/frontend/js/classes/video-tool.js

387 lines
12 KiB
JavaScript
Raw Normal View History

2025-05-26 08:17:24 +03:00
export default class VideoTool {
static get toolbox() {
return {
icon: `<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M16.25 3.75H3.75C2.36929 3.75 1.25 4.86929 1.25 6.25V13.75C1.25 15.1307 2.36929 16.25 3.75 16.25H16.25C17.6307 16.25 18.75 15.1307 18.75 13.75V6.25C18.75 4.86929 17.6307 3.75 16.25 3.75Z" stroke="currentColor" stroke-width="1.5"/><path d="M8.125 6.875L13.125 10L8.125 13.125V6.875Z" fill="currentColor"/></svg>`,
title: 'Video',
};
}
2025-05-26 18:47:04 +03:00
constructor({ data, api, config, block }) { // <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> block <20> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
2025-05-26 08:17:24 +03:00
this.data = data || {};
this.api = api;
this.config = config || {};
2025-05-26 18:47:04 +03:00
this.block = block; // <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>
2025-05-26 08:17:24 +03:00
this.wrapper = null;
this.alignment = data.alignment || 'center';
this.width = data.width || '100%';
2025-05-26 18:47:04 +03:00
this.filetype = data.filetype || 'file';
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
2025-05-26 08:17:24 +03:00
this._handleFileUpload = this._handleFileUpload.bind(this);
this._initResize = this._initResize.bind(this);
2025-05-26 18:47:04 +03:00
this._handleUrlSubmit = this._handleUrlSubmit.bind(this);
}
2025-05-26 08:17:24 +03:00
render() {
this.wrapper = document.createElement('div');
this.wrapper.classList.add('video-tool');
if (this.data.url) {
2025-05-26 18:47:04 +03:00
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
const filetype = this.data.filetype ||
(this.data.url.includes('youtube') ? 'youtube' :
this.data.url.includes('rutube') ? 'rutube' : 'file');
this._createVideoElement(this.data.url, this.data.caption || '', filetype);
2025-05-26 08:17:24 +03:00
} else {
this._createUploadForm();
}
return this.wrapper;
}
_createUploadForm() {
2025-05-26 18:47:04 +03:00
const currentUrl = this.wrapper?.querySelector('.embed-url')?.value || '';
2025-05-26 08:17:24 +03:00
this.wrapper.innerHTML = `
<div class="video-upload-tabs">
<div class="tabs-header">
<button class="tab-button active" data-tab="upload">Upload</button>
<button class="tab-button" data-tab="embed">Embed URL</button>
</div>
2025-05-26 18:47:04 +03:00
<div class="tab-content active" data-tab="upload">
2025-05-26 08:17:24 +03:00
<label class="video-upload-button">
<input type="file" accept="video/mp4,video/webm,video/ogg" class="video-file-input">
<span>Select Video File</span>
</label>
</div>
2025-05-26 18:47:04 +03:00
<div class="tab-content" data-tab="embed">
2025-05-26 08:17:24 +03:00
<div class="embed-form">
<select class="embed-service">
<option value="youtube">YouTube</option>
<option value="rutube">Rutube</option>
</select>
<input type="text" class="embed-url" placeholder="Paste video URL...">
2025-05-26 18:47:04 +03:00
<button class="embed-submit"><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD></button>
2025-05-26 08:17:24 +03:00
</div>
</div>
</div>
`;
2025-05-26 18:47:04 +03:00
this._bindFormHandlers();
if (currentUrl) {
const urlInput = this.wrapper.querySelector('.embed-url');
if (urlInput) urlInput.value = currentUrl;
}
}
_bindFormHandlers() {
// Tab switching
2025-05-26 08:17:24 +03:00
this.wrapper.querySelectorAll('.tab-button').forEach(btn => {
btn.addEventListener('click', () => {
this.wrapper.querySelectorAll('.tab-button, .tab-content').forEach(el => {
el.classList.toggle('active', el.dataset.tab === btn.dataset.tab);
});
2025-05-26 18:47:04 +03:00
// <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
this._bindFormHandlers();
2025-05-26 08:17:24 +03:00
});
});
// File upload handler
2025-05-26 18:47:04 +03:00
const fileInput = this.wrapper.querySelector('.video-file-input');
if (fileInput) {
fileInput.addEventListener('change', this._handleFileUpload);
}
2025-05-26 08:17:24 +03:00
// URL embed handler
2025-05-26 18:47:04 +03:00
const embedSubmit = this.wrapper.querySelector('.embed-submit');
if (embedSubmit) {
embedSubmit.addEventListener('click', this._handleUrlSubmit);
}
}
2025-05-26 08:17:24 +03:00
async _handleFileUpload(event) {
2025-05-26 18:47:04 +03:00
if (!event.target.files || event.target.files.length === 0) {
return;
}
2025-05-26 08:17:24 +03:00
const file = event.target.files[0];
if (!file) return;
if (!this.config.uploader || !this.config.uploader.byFile) {
this._showError('File upload is not configured');
return;
}
try {
const url = await this._uploadFile(file);
this.type = 'file';
this.data = { url, type: 'file' };
2025-05-26 18:47:04 +03:00
this._createVideoElement(url, '');
2025-05-26 08:17:24 +03:00
} catch (error) {
this._showError('File upload failed: ' + error.message);
}
}
_handleUrlSubmit() {
2025-05-26 18:47:04 +03:00
const wrapper = this.wrapper;
2025-05-26 08:17:24 +03:00
const service = wrapper.querySelector('.embed-service').value;
2025-05-26 18:47:04 +03:00
const urlInput = wrapper.querySelector('.embed-url');
const url = urlInput.value.trim();
2025-05-26 08:17:24 +03:00
if (!url) {
2025-05-26 18:47:04 +03:00
this._showError('<27><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>, <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> URL <20><><EFBFBD><EFBFBD><EFBFBD>');
return;
2025-05-26 08:17:24 +03:00
}
try {
2025-05-26 18:47:04 +03:00
const submitBtn = wrapper.querySelector('.embed-submit');
submitBtn.disabled = true;
submitBtn.textContent = '<27><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>...';
const embedUrl = this._parseEmbedUrl(service, url);
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>
this.data = {
url: embedUrl,
filetype: service,
width: this.width,
alignment: this.alignment,
caption: ''
};
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
this.api.blocks.update(this.block.id, this.data);
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
this.wrapper.innerHTML = '';
this._createVideoElement(embedUrl, '');
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
submitBtn.disabled = false;
submitBtn.textContent = '<27><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>';
2025-05-26 08:17:24 +03:00
} catch (error) {
2025-05-26 18:47:04 +03:00
this._showError(error.message);
const submitBtn = wrapper.querySelector('.embed-submit');
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.textContent = '<27><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>';
}
2025-05-26 08:17:24 +03:00
}
2025-05-26 18:47:04 +03:00
}
2025-05-26 08:17:24 +03:00
_parseEmbedUrl(service, url) {
// YouTube
if (service === 'youtube') {
2025-05-26 18:47:04 +03:00
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> YouTube <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
const regExp = /(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/i;
const match = url.match(regExp);
if (match && match[1]) {
return `https://www.youtube.com/embed/${match[1]}`;
}
2025-05-26 08:17:24 +03:00
}
// Rutube
if (service === 'rutube') {
2025-05-26 18:47:04 +03:00
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Rutube <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
const regExp = /rutube\.ru\/(?:video\/|play\/embed\/|video\/embed\/)?([a-zA-Z0-9]+)/i;
2025-05-26 08:17:24 +03:00
const match = url.match(regExp);
if (match && match[1]) {
2025-05-26 18:47:04 +03:00
return `https://rutube.ru/play/embed/${match[1]}`;
2025-05-26 08:17:24 +03:00
}
2025-05-26 18:47:04 +03:00
throw new Error('<27><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> URL Rutube. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: https://rutube.ru/video/CODE/');
2025-05-26 08:17:24 +03:00
}
2025-05-26 18:47:04 +03:00
throw new Error('Invalid URL.');
}
_createVideoElement(url, caption, filetype = null) {
this.wrapper.innerHTML = '';
const container = document.createElement('div');
container.className = 'video-container';
container.style.margin = this._getAlignmentMargin();
container.style.width = this.width;
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
const type = filetype || this.filetype ||
(url.match(/\.(mp4|webm|ogg)$/i) ? 'file' :
(url.includes('youtube.com/embed') || url.includes('youtu.be') ? 'youtube' :
(url.includes('rutube.ru/embed') || url.includes('rutube.ru/video') ? 'rutube' : 'file')));
2025-05-26 08:17:24 +03:00
2025-05-26 18:47:04 +03:00
const mediaContainer = document.createElement('div');
mediaContainer.style.position = 'relative';
mediaContainer.style.paddingBottom = '56.25%';
mediaContainer.style.height = '0';
mediaContainer.style.overflow = 'hidden';
if (type === 'file') {
2025-05-26 08:17:24 +03:00
const video = document.createElement('video');
2025-05-26 18:47:04 +03:00
video.src = url;
2025-05-26 08:17:24 +03:00
video.controls = true;
2025-05-26 18:47:04 +03:00
video.style.position = 'absolute';
video.style.top = '0';
video.style.left = '0';
2025-05-26 08:17:24 +03:00
video.style.width = '100%';
2025-05-26 18:47:04 +03:00
video.style.height = '100%';
mediaContainer.appendChild(video);
} else {
2025-05-26 08:17:24 +03:00
const iframe = document.createElement('iframe');
2025-05-26 18:47:04 +03:00
iframe.src = url.includes('rutube.ru/video') ?
url.replace('rutube.ru/video', 'rutube.ru/play/embed') : url;
2025-05-26 08:17:24 +03:00
iframe.frameBorder = '0';
iframe.allowFullscreen = true;
2025-05-26 18:47:04 +03:00
iframe.allow = "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture";
iframe.style.position = 'absolute';
iframe.style.top = '0';
iframe.style.left = '0';
2025-05-26 08:17:24 +03:00
iframe.style.width = '100%';
2025-05-26 18:47:04 +03:00
iframe.style.height = '100%';
mediaContainer.appendChild(iframe);
2025-05-26 08:17:24 +03:00
}
2025-05-26 18:47:04 +03:00
container.appendChild(mediaContainer);
this._addCaption(container, caption);
this._addResizeHandle(container);
this.wrapper.appendChild(container);
}
2025-05-26 08:17:24 +03:00
2025-05-26 18:47:04 +03:00
_addCaption(container, captionText) {
if (!captionText) return;
const caption = document.createElement('div');
caption.className = 'video-caption';
caption.contentEditable = true;
caption.innerHTML = captionText;
container.appendChild(caption);
}
2025-05-26 08:17:24 +03:00
2025-05-26 18:47:04 +03:00
async _uploadFile(file) {
try {
const formData = new FormData();
formData.append('file', file);
const response = await fetch(this.config.uploader.byFile, {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (!data?.file?.url) {
throw new Error('Invalid response format');
}
return data.file.url;
} catch (error) {
console.error('Upload error:', error);
throw error;
}
2025-05-26 08:17:24 +03:00
}
2025-05-26 18:47:04 +03:00
_addResizeHandle(container) {
const video = container.querySelector('video, iframe');
2025-05-26 08:17:24 +03:00
2025-05-26 18:47:04 +03:00
if (!video) return; // <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>, <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
2025-05-26 08:17:24 +03:00
const handle = document.createElement('div');
handle.className = 'resize-handle';
2025-05-26 18:47:04 +03:00
handle.innerHTML = '-';
handle.addEventListener('mousedown', (e) => {
e.preventDefault();
const startWidth = parseInt(video.style.width) || video.offsetWidth;
const startX = e.clientX;
const doResize = (e) => {
const newWidth = Math.max(200, startWidth + (e.clientX - startX));
video.style.width = `${newWidth}px`;
};
const stopResize = () => {
document.removeEventListener('mousemove', doResize);
document.removeEventListener('mouseup', stopResize);
this.width = video.style.width;
};
document.addEventListener('mousemove', doResize);
document.addEventListener('mouseup', stopResize, { once: true });
});
2025-05-26 08:17:24 +03:00
container.appendChild(handle);
}
_initResize(e) {
e.preventDefault();
const video = this.wrapper.querySelector('video');
const startWidth = parseInt(video.style.width) || video.offsetWidth;
const startX = e.clientX;
const doResize = (e) => {
const newWidth = Math.max(200, startWidth + (e.clientX - startX));
video.style.width = `${newWidth}px`;
};
const stopResize = () => {
document.removeEventListener('mousemove', doResize);
document.removeEventListener('mouseup', stopResize);
this.width = video.style.width;
};
document.addEventListener('mousemove', doResize);
document.addEventListener('mouseup', stopResize, { once: true });
}
save(blockContent) {
2025-05-26 18:47:04 +03:00
const video = blockContent.querySelector('video, iframe');
2025-05-26 08:17:24 +03:00
const caption = blockContent.querySelector('.video-caption');
return {
2025-05-26 18:47:04 +03:00
url: video?.src || this.data.url || '',
caption: caption?.innerHTML || this.data.caption || '',
width: this.width || '100%',
alignment: this.alignment,
filetype: this.filetype || (video?.tagName === 'IFRAME' ?
(video.src.includes('youtube') ? 'youtube' : 'rutube')
: 'file')
2025-05-26 08:17:24 +03:00
};
}
_getAlignmentMargin() {
switch(this.alignment) {
case 'left': return '0 auto 0 0';
case 'right': return '0 0 0 auto';
default: return '0 auto';
}
}
_showError(message) {
2025-05-26 18:47:04 +03:00
if (!this.wrapper) return;
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>
const currentTab = this.wrapper.querySelector('.tab-button.active').dataset.tab;
const currentUrl = this.wrapper.querySelector('.embed-url')?.value || '';
this.wrapper.innerHTML = `
<div class="video-error">
<p>${message}</p>
<button class="retry-button"><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD></button>
</div>
`;
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
this.wrapper.querySelector('.retry-button').addEventListener('click', () => {
this._createUploadForm();
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
if (currentTab === 'embed') {
const embedUrl = this.wrapper.querySelector('.embed-url');
if (embedUrl) embedUrl.value = currentUrl;
}
});
}
2025-05-26 08:17:24 +03:00
}