1
0
Fork 0
mirror of https://github.com/codex-team/codex.docs.git synced 2025-07-19 05:09:41 +02:00

added new block

This commit is contained in:
exezzz 2025-06-01 21:12:29 +03:00
parent 28580e300b
commit 680bcde28c
12 changed files with 949 additions and 395 deletions

60
package-lock.json generated
View file

@ -9,6 +9,7 @@
"version": "2.2.3",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/abort-controller": "^3.374.0",
"@aws-sdk/client-s3": "^3.181.0",
"@codex-team/config-loader": "0.1.0-rc1",
"@codexteam/shortcuts": "^1.2.0",
@ -20,6 +21,7 @@
"cookie-parser": "^1.4.5",
"csurf": "^1.2.2",
"debug": "^4.3.2",
"editorjs-header-with-alignment": "^1.0.1",
"express": "^4.17.1",
"file-type": "^16.5.4",
"fs-extra": "^10.1.0",
@ -50,9 +52,9 @@
"@babel/polyfill": "^7.12.1",
"@babel/preset-env": "^7.16.11",
"@codexteam/misprints": "^1.0.0",
"@coolbytes/editorjs-delimiter": "^1.0.3",
"@editorjs/checklist": "^1.3.0",
"@editorjs/code": "^2.7.0",
"@editorjs/delimiter": "^1.2.0",
"@editorjs/editorjs": "^2.25.0",
"@editorjs/embed": "^2.5.1",
"@editorjs/header": "^2.6.2",
@ -427,17 +429,39 @@
}
},
"node_modules/@aws-sdk/abort-controller": {
"version": "3.178.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/abort-controller/-/abort-controller-3.178.0.tgz",
"integrity": "sha512-ptDkCB06BJrYdhKzamM9yI15LxcGkPczY80hzKAY/aecm09alnW27uCt5HJJx2nCd18IUH28ZO1sc7DTLOWb3A==",
"license": "Apache-2.0",
"peer": true,
"version": "3.374.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/abort-controller/-/abort-controller-3.374.0.tgz",
"integrity": "sha512-pO1pqFBdIF28ZvnJmg58Erj35RLzXsTrjvHghdc/xgtSvodFFCNrUsPg6AP3On8eiw9elpHoS4P8jMx1pHDXEw==",
"deprecated": "This package has moved to @smithy/abort-controller",
"dependencies": {
"@aws-sdk/types": "3.178.0",
"tslib": "^2.3.1"
"@smithy/abort-controller": "^1.0.1",
"tslib": "^2.5.0"
},
"engines": {
"node": ">= 12.0.0"
"node": ">=14.0.0"
}
},
"node_modules/@aws-sdk/abort-controller/node_modules/@smithy/abort-controller": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-1.1.0.tgz",
"integrity": "sha512-5imgGUlZL4dW4YWdMYAKLmal9ny/tlenM81QZY7xYyb76z9Z/QOg7oM5Ak9HQl8QfFTlGVWwcMXl+54jroRgEQ==",
"dependencies": {
"@smithy/types": "^1.2.0",
"tslib": "^2.5.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@aws-sdk/abort-controller/node_modules/@smithy/types": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-1.2.0.tgz",
"integrity": "sha512-z1r00TvBqF3dh4aHhya7nz1HhvCg4TRmw51fjMrh5do3h+ngSstt/yKlNbHeb9QxJmFbmN8KEVSWgb1bRvfEoA==",
"dependencies": {
"tslib": "^2.5.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@aws-sdk/client-cognito-identity": {
@ -3524,6 +3548,12 @@
"integrity": "sha512-Udb8lkwhXEiPoLm7krtUv2f8jYQTutHxsLecmsMvMbOxMJ49LA/EUUzn8Fo32mxOFrI7qozOovspLhHb+y60nQ==",
"license": "MIT"
},
"node_modules/@coolbytes/editorjs-delimiter": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@coolbytes/editorjs-delimiter/-/editorjs-delimiter-1.0.3.tgz",
"integrity": "sha512-Ix0z5ujv12Y+JXkUypxtdX36OsDBNyjLo/k8oPxBcq6UxCS1F96dXvE9XRb/tpN6qW8BbWUfViyBbeOLAbGkLw==",
"dev": true
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
@ -3600,13 +3630,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/@editorjs/delimiter": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@editorjs/delimiter/-/delimiter-1.2.0.tgz",
"integrity": "sha512-GKsCFPk85vH5FuCuVQ48NTLc9hk0T3DsBH9zABaicTYIJayFcUa8N4/Y+L3i4tduzDqqyvoxkv+5n43GmC5gEA==",
"dev": true,
"license": "MIT"
},
"node_modules/@editorjs/editorjs": {
"version": "2.30.8",
"resolved": "https://registry.npmjs.org/@editorjs/editorjs/-/editorjs-2.30.8.tgz",
@ -7964,6 +7987,11 @@
"safe-buffer": "^5.0.1"
}
},
"node_modules/editorjs-header-with-alignment": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/editorjs-header-with-alignment/-/editorjs-header-with-alignment-1.0.1.tgz",
"integrity": "sha512-L7cDSx/H3wgb9dg05HH9xPMotj3tkoElTd0TeY81pxvO0+tPV+yRZ9hV7fUyZolYUObKfYL9QS+yeTY/YCht5g=="
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",

View file

@ -22,7 +22,7 @@
"test:js": "cross-env NODE_ENV=testing mocha --recursive ./dist/test --exit",
"test": "cross-env NODE_ENV=testing ts-mocha -n loader=ts-node/esm ./src/test/*.ts ./src/test/**/*.ts --exit ",
"lint": "eslint --fix --ext .ts ./src/backend",
"editor-upgrade": "yarn add -D @editorjs/{editorjs,header,code,delimiter,list,link,image,table,inline-code,marker,warning,checklist,raw}@latest"
"editor-upgrade": "yarn add -D @editorjs/{editorjs,header,code,list,link,image,table,inline-code,marker,warning,checklist,raw}@latest"
},
"dependencies": {
"@aws-sdk/abort-controller": "^3.374.0",
@ -37,6 +37,8 @@
"cookie-parser": "^1.4.5",
"csurf": "^1.2.2",
"debug": "^4.3.2",
"editorjs-header-with-alignment": "^1.0.1",
"editorjs-alert": "^1.1.4",
"express": "^4.17.1",
"file-type": "^16.5.4",
"fs-extra": "^10.1.0",
@ -66,10 +68,11 @@
"@codexteam/misprints": "^1.0.0",
"@editorjs/checklist": "^1.3.0",
"@editorjs/code": "^2.7.0",
"@editorjs/delimiter": "^1.2.0",
"@coolbytes/editorjs-delimiter": "^1.0.3",
"@editorjs/editorjs": "^2.25.0",
"@editorjs/embed": "^2.5.1",
"@editorjs/header": "^2.6.2",
"@editorjs/paragraph": "^2.11.7",
"@editorjs/image": "^2.6.2",
"@editorjs/inline-code": "^1.3.1",
"@editorjs/link": "^2.4.0",

View file

@ -102,16 +102,23 @@ function createApp(): express.Express {
if (appConfig.hawk?.backendToken && err instanceof Error) {
HawkCatcher.send(err);
}
// CSRF error (from csurf)
if (err && typeof err === 'object' && 'code' in err && (err as any).code === 'EBADCSRFTOKEN') {
res.status(403);
return res.render('error', { status: 403, message: 'Invalid CSRF token' });
}
// only send Http based exception to client.
if (err instanceof HttpException) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
if (err.status === 403) {
return res.render('error', { status: 403, message: 'Доступ запрещён' });
}
return res.render('error');
}
next(err);
// fallback for other errors
res.status(500).render('error', { status: 500, message: 'Internal Server Error' });
});
return app;

View file

@ -0,0 +1,10 @@
<div class="cdx-alert cdx-alert-{{ type }}{% if align %} cdx-alert-align-{{ align }}{% endif %}">
{% if title is not empty %}
<div class="cdx-alert__title">
{{ title }}
</div>
{% endif %}
<div class="cdx-alert__message">
{{ message }}
</div>
</div>

View file

@ -1,28 +1,27 @@
{% set width = data.width|default('100%') %}
{% set alignment = data.alignment|default('center') %}
{% set classes = ['block-image__content'] %}
<figure class="block-image" style="width: {{ width }}; max-width: 100%; margin:
{% if alignment == 'left' %}0 auto 0 0
{% elseif alignment == 'right' %}0 0 0 auto
{% else %}0 auto
{% endif %};">
{% if withBorder %}
{% set classes = classes|merge(['block-image__content--bordered']) %}
{% endif %}
{% if withBackground %}
{% set classes = classes|merge(['block-image__content--with-background']) %}
{% endif %}
<figure class="block-image">
<div class="{{ classes.join(' ') }}">
{% if file.mime and file.mime == 'video/mp4' %}
<video controls style="width: 100%;">
<video autoplay loop muted playsinline>
<source src="{{ file.url }}" type="video/mp4">
</video>
{% else %}
<img
src="{{ file.url }}"
alt="{{ caption ? caption | striptags : '' }}"
style="width: {{ width }};"
>
<img src="{{ file.url }}" alt="{{ caption ? caption | striptags : '' }}">
{% endif %}
</div>
{% if caption %}
<footer class="block-image__caption">
{{ caption }}
</footer>
<footer class="block-image__caption">
{{ caption }}
</footer>
{% endif %}
</figure>

View file

@ -1,9 +1,7 @@
{% set width = width|default('100%') %}
{% set height = height|default('auto') %}
{% set alignment = alignment|default('center') %}
{% set filetype = filetype|default('file') %}
<figure class="block-video" style="width: {{ width }}; max-width: 100%; margin:
<figure class="block-video" style="margin:
{% if alignment == 'left' %}0 auto 0 0
{% elseif alignment == 'right' %}0 0 0 auto
{% else %}0 auto
@ -15,20 +13,14 @@
frameborder="0"
allowfullscreen
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"
></iframe>
{% else %}
<video
controls
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"
{% if width %}width="{{ width }}"{% endif %}
{% if height %}height="{{ height }}"{% endif %}
>
<video controls>
<source src="{{ url }}" type="{{ mime|default('video/mp4') }}">
Âàø áðàóçåð íå ïîääåðæèâàåò âèäåî òåã.
Ваш браузер не поддерживает видео тег.
</video>
{% endif %}
</div>
</div>
{% if caption %}
<footer class="block-video__caption">
{{ caption|raw }}

View file

@ -36,7 +36,7 @@
{% for block in page.body.blocks %}
{# Skip first header, because it is already showed as a Title #}
{% if not (loop.first and block.type == 'header') %}
{% if block.type in ['paragraph', 'header', 'image', 'code', 'list', 'delimiter', 'table', 'warning', 'checklist', 'linkTool', 'raw', 'embed','video'] %}
{% if block.type in ['paragraph', 'header', 'image', 'code', 'list', 'delimiter', 'table', 'warning', 'checklist', 'linkTool', 'raw', 'embed', 'video', 'alert'] %}
<div class="page__content-block">
{% include './blocks/' ~ block.type ~ '.twig' with block.data %}
</div>

View file

@ -1,126 +0,0 @@
import ImageTool from '@editorjs/image';
export default class CustomImageTool extends ImageTool {
static get toolbox() {
return ImageTool.toolbox; // Возвращаем стандартную иконку
}
constructor({ data, api, config }) {
super({ data, api, config });
this.alignment = data.alignment || 'center';
this.width = data.width || '100%';
}
render() {
const container = super.render();
this._container = container;
// Добавляем обработчики после полного рендера
setTimeout(() => {
const wrapper = container.querySelector('.cdx-image');
if (wrapper) {
this._applyStyles(wrapper);
this._addControls(wrapper);
}
}, 100);
return container;
}
_applyStyles(wrapper) {
wrapper.style.display = 'block';
wrapper.style.width = 'fit-content';
wrapper.style.margin = this._getAlignmentMargin();
const img = wrapper.querySelector('img');
if (img) {
img.style.width = this.width;
img.style.height = 'auto';
}
}
_getAlignmentMargin() {
switch(this.alignment) {
case 'left': return '0 auto 0 0';
case 'right': return '0 0 0 auto';
default: return '0 auto';
}
}
_addControls(wrapper) {
// Добавляем кнопки выравнивания
const alignControls = document.createElement('div');
alignControls.className = 'image-align-controls';
['left', 'center', 'right'].forEach(align => {
const btn = document.createElement('button');
btn.innerHTML = this._getAlignmentIcon(align);
btn.dataset.align = align;
btn.classList.toggle('active', this.alignment === align);
btn.addEventListener('click', () => {
this.alignment = align;
wrapper.style.margin = this._getAlignmentMargin();
alignControls.querySelectorAll('button').forEach(b => {
b.classList.toggle('active', b.dataset.align === align);
});
});
alignControls.appendChild(btn);
});
// Добавляем ручку ресайза
const resizeHandle = document.createElement('div');
resizeHandle.className = 'image-resize-handle';
resizeHandle.innerHTML = '↔';
resizeHandle.addEventListener('mousedown', (e) => {
e.preventDefault();
const img = wrapper.querySelector('img');
if (img) this._initResize(img, e);
});
wrapper.appendChild(alignControls);
wrapper.appendChild(resizeHandle);
}
_initResize(element, startEvent) {
const startWidth = parseInt(element.style.width) || element.offsetWidth;
const startX = startEvent.clientX;
const maxWidth = this._container.offsetWidth;
const doResize = (e) => {
const newWidth = Math.max(200, Math.min(startWidth + (e.clientX - startX), maxWidth));
element.style.width = `${newWidth}px`;
};
const stopResize = () => {
document.removeEventListener('mousemove', doResize);
document.removeEventListener('mouseup', stopResize);
this.width = element.style.width;
};
document.addEventListener('mousemove', doResize);
document.addEventListener('mouseup', stopResize, { once: true });
}
save(blockContent) {
const savedData = super.save(blockContent);
return {
...savedData,
width: this.width,
alignment: this.alignment
};
}
_getAlignmentIcon(align) {
const icons = {
left: '⎸',
center: '⎸⎹',
right: ' |'
};
return icons[align] || '';
}
}

View file

@ -4,12 +4,15 @@ import EditorJS from '@editorjs/editorjs';
* Block Tools for the Editor
*/
import Header from '@editorjs/header';
import Paragraph from '@editorjs/paragraph';
import Image from '@editorjs/image';
import CodeTool from '@editorjs/code';
import List from '@editorjs/list';
import Delimiter from '@editorjs/delimiter';
//import Delimiter from '@editorjs/delimiter';
import Delimiter from '@coolbytes/editorjs-delimiter';
import Table from '@editorjs/table';
import Warning from '@editorjs/warning';
import Alert from 'editorjs-alert';
import Checklist from '@editorjs/checklist';
import LinkTool from '@editorjs/link';
import RawTool from '@editorjs/raw';
@ -23,7 +26,6 @@ import Marker from '@editorjs/marker';
import { TextColorTool } from './text-color-tool.js';
import { TextSizeTool } from './text-size-tool.js';
import VideoTool from './video-tool.js';
import CustomImageTool from './custom-image-tool.js';
/*
* Class for working with Editor.js
*/
@ -50,7 +52,11 @@ export default class Editor {
placeholder: options.headerPlaceholder || '',
},
},
paragraph: {
class: Paragraph,
inlineToolbar: true,
},
//header: Header,
textColor: {
class: TextColorTool,
config: {
@ -124,6 +130,8 @@ export default class Editor {
inlineToolbar: true,
},
alert: Alert,
checklist: {
class: Checklist,
inlineToolbar: true,
@ -146,13 +154,14 @@ export default class Editor {
embed: Embed,
},
defaultBlock: 'Paragraph',
data: {
blocks: [
{
type: 'header',
data: {
text: '',
level: 2,
level: 3,
},
},
],
@ -170,4 +179,6 @@ export default class Editor {
save() {
return this.editor.saver.save();
}
}

View file

@ -6,28 +6,24 @@ export default class VideoTool {
};
}
constructor({ data, api, config, block }) { // Äîáàâëÿåì block â ïàðàìåòðû
constructor({ data, api, config, block }) {
this.data = data || {};
this.api = api;
this.config = config || {};
this.block = block; // Ñîõðàíÿåì ññûëêó íà òåêóùèé áëîê
this.block = block;
this.wrapper = null;
this.alignment = data.alignment || 'center';
this.width = data.width || '100%';
this.filetype = data.filetype || 'file';
// Ïðèâÿçûâàåì êîíòåêñò äëÿ îáðàáîò÷èêîâ
this._handleFileUpload = this._handleFileUpload.bind(this);
this._initResize = this._initResize.bind(this);
this._handleUrlSubmit = this._handleUrlSubmit.bind(this);
}
}
render() {
this.wrapper = document.createElement('div');
this.wrapper.classList.add('video-tool');
if (this.data.url) {
// Äîáàâëÿåì ïðîâåðêó äëÿ ñîâìåñòèìîñòè ñî ñòàðûìè äàííûìè
const filetype = this.data.filetype ||
(this.data.url.includes('youtube') ? 'youtube' :
this.data.url.includes('rutube') ? 'rutube' : 'file');
@ -61,7 +57,7 @@ export default class VideoTool {
<option value="rutube">Rutube</option>
</select>
<input type="text" class="embed-url" placeholder="Paste video URL...">
<button class="embed-submit">Âñòàâèòü âèäåî</button>
<button class="embed-submit">Добавить</button>
</div>
</div>
</div>
@ -75,35 +71,31 @@ export default class VideoTool {
}
_bindFormHandlers() {
// Tab switching
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);
});
// Ïîñëå ïåðåêëþ÷åíèÿ âêëàäêè ïåðåïðèâÿçûâàåì îáðàáîò÷èêè
this._bindFormHandlers();
});
});
// File upload handler
const fileInput = this.wrapper.querySelector('.video-file-input');
if (fileInput) {
fileInput.addEventListener('change', this._handleFileUpload);
}
// URL embed handler
const embedSubmit = this.wrapper.querySelector('.embed-submit');
if (embedSubmit) {
embedSubmit.addEventListener('click', this._handleUrlSubmit);
}
}
}
async _handleFileUpload(event) {
if (!event.target.files || event.target.files.length === 0) {
return;
}
}
const file = event.target.files[0];
if (!file) return;
@ -129,51 +121,44 @@ export default class VideoTool {
const url = urlInput.value.trim();
if (!url) {
this._showError('Ïîæàëóéñòà, ââåäèòå URL âèäåî');
this._showError(', URL ');
return;
}
try {
const submitBtn = wrapper.querySelector('.embed-submit');
submitBtn.disabled = true;
submitBtn.textContent = 'Îáðàáîòêà...';
submitBtn.textContent = '...';
const embedUrl = this._parseEmbedUrl(service, url);
// Îáíîâëÿåì äàííûå òåêóùåãî áëîêà
this.data = {
url: embedUrl,
filetype: service,
width: this.width,
alignment: this.alignment,
caption: ''
};
// Ñîõðàíÿåì èçìåíåíèÿ
this.api.blocks.update(this.block.id, this.data);
// Ïåðåñîçäàåì ýëåìåíò
this.wrapper.innerHTML = '';
this._createVideoElement(embedUrl, '');
// Âîññòàíàâëèâàåì êíîïêó
submitBtn.disabled = false;
submitBtn.textContent = 'Âñòàâèòü âèäåî';
submitBtn.textContent = ' ';
} catch (error) {
this._showError(error.message);
const submitBtn = wrapper.querySelector('.embed-submit');
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.textContent = 'Âñòàâèòü âèäåî';
submitBtn.textContent = ' ';
}
}
}
}
_parseEmbedUrl(service, url) {
// YouTube
if (service === 'youtube') {
// Ïîääåðæêà âñåõ ôîðìàòîâ YouTube ññûëîê
const regExp = /(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/i;
const match = url.match(regExp);
if (match && match[1]) {
@ -181,71 +166,66 @@ export default class VideoTool {
}
}
// Rutube
if (service === 'rutube') {
// Ïîääåðæêà âñåõ ôîðìàòîâ Rutube ññûëîê
const regExp = /rutube\.ru\/(?:video\/|play\/embed\/|video\/embed\/)?([a-zA-Z0-9]+)/i;
const match = url.match(regExp);
if (match && match[1]) {
return `https://rutube.ru/play/embed/${match[1]}`;
}
throw new Error('Íåâåðíûé URL Rutube. Ïðèìåð ïðàâèëüíîãî ôîðìàòà: https://rutube.ru/video/CODE/');
}
throw new Error(' URL Rutube. : https://rutube.ru/video/CODE/');
}
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;
// Îïðåäåëÿåì òèï êîíòåíòà
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')));
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') {
const video = document.createElement('video');
video.src = url;
video.controls = true;
video.style.position = 'absolute';
video.style.top = '0';
video.style.left = '0';
video.style.width = '100%';
video.style.height = '100%';
mediaContainer.appendChild(video);
} else {
const iframe = document.createElement('iframe');
iframe.src = url.includes('rutube.ru/video') ?
url.replace('rutube.ru/video', 'rutube.ru/play/embed') : url;
iframe.frameBorder = '0';
iframe.allowFullscreen = true;
iframe.allow = "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture";
iframe.style.position = 'absolute';
iframe.style.top = '0';
iframe.style.left = '0';
iframe.style.width = '100%';
iframe.style.height = '100%';
mediaContainer.appendChild(iframe);
}
container.appendChild(mediaContainer);
this._addCaption(container, caption);
this._addResizeHandle(container);
this.wrapper.appendChild(container);
}
_createVideoElement(url, caption, filetype = null) {
this.wrapper.innerHTML = '';
const container = document.createElement('div');
container.className = 'video-container';
container.style.margin = this._getAlignmentMargin();
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')));
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') {
const video = document.createElement('video');
video.src = url;
video.controls = true;
video.style.position = 'absolute';
video.style.top = '0';
video.style.left = '0';
video.style.width = '100%';
video.style.height = '100%';
mediaContainer.appendChild(video);
} else {
const iframe = document.createElement('iframe');
iframe.src = url.includes('rutube.ru/video') ?
url.replace('rutube.ru/video', 'rutube.ru/play/embed') : url;
iframe.frameBorder = '0';
iframe.allowFullscreen = true;
iframe.allow = "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture";
iframe.style.position = 'absolute';
iframe.style.top = '0';
iframe.style.left = '0';
iframe.style.width = '100%';
iframe.style.height = '100%';
mediaContainer.appendChild(iframe);
}
container.appendChild(mediaContainer);
this._addCaption(container, caption);
this.wrapper.appendChild(container);
}
_addCaption(container, captionText) {
if (!captionText) return;
@ -284,59 +264,6 @@ _createVideoElement(url, caption, filetype = null) {
}
}
_addResizeHandle(container) {
const video = container.querySelector('video, iframe');
if (!video) return; // Åñëè âèäåî íå íàéäåíî, âûõîäèì
const handle = document.createElement('div');
handle.className = 'resize-handle';
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 });
});
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) {
const video = blockContent.querySelector('video, iframe');
const caption = blockContent.querySelector('.video-caption');
@ -344,7 +271,6 @@ _createVideoElement(url, caption, filetype = null) {
return {
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')
@ -363,25 +289,22 @@ _createVideoElement(url, caption, filetype = null) {
_showError(message) {
if (!this.wrapper) return;
// Ñîõðàíÿåì òåêóùåå ñîñòîÿíèå ôîðìû
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">Ïîïðîáîâàòü ñíîâà</button>
<button class="retry-button"> </button>
</div>
`;
// Îáðàáîò÷èê äëÿ êíîïêè ïîâòîðà
this.wrapper.querySelector('.retry-button').addEventListener('click', () => {
this._createUploadForm();
// Âîññòàíàâëèâàåì ñîñòîÿíèå
if (currentTab === 'embed') {
const embedUrl = this.wrapper.querySelector('.embed-url');
if (embedUrl) embedUrl.value = currentUrl;
}
});
}
}
}

View file

@ -374,6 +374,73 @@
}
}
/**
* Alert (Editor.js style)
* ==================
*/
.cdx-alert {
position: relative;
padding: 10px;
border-radius: 5px;
margin-bottom: 10px;
}
.cdx-alert__title {
font-weight: bold;
margin-bottom: 0.5rem;
}
.cdx-alert__message {
outline: none;
}
.cdx-alert-align-left {
text-align: left;
}
.cdx-alert-align-center {
text-align: center;
}
.cdx-alert-align-right {
text-align: right;
}
.cdx-alert-primary {
background-color: #ebf8ff;
border: 1px solid #4299e1;
color: #2b6cb0;
}
.cdx-alert-secondary {
background-color: #f7fafc;
border: 1px solid #cbd5e0;
color: #222731;
}
.cdx-alert-info {
background-color: #e6fdff;
border: 1px solid #4cd4ce;
color: #00727c;
}
.cdx-alert-success {
background-color: #f0fff4;
border: 1px solid #68d391;
color: #2f855a;
}
.cdx-alert-warning {
background-color: #fffaf0;
border: 1px solid #ed8936;
color: #c05621;
}
.cdx-alert-danger {
background-color: #fff5f5;
border: 1px solid #fc8181;
color: #c53030;
}
.cdx-alert-light {
background-color: #fff;
border: 1px solid #edf2f7;
color: #1a202c;
}
.cdx-alert-dark {
background-color: #2d3748;
border: 1px solid #1a202c;
color: #d3d3d3;
}
/**
* Checklist
* ==================

788
yarn.lock

File diff suppressed because it is too large Load diff