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:
parent
28580e300b
commit
680bcde28c
12 changed files with 949 additions and 395 deletions
60
package-lock.json
generated
60
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
|
10
src/backend/views/pages/blocks/alert.twig
Normal file
10
src/backend/views/pages/blocks/alert.twig
Normal 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>
|
|
@ -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>
|
|
@ -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 }}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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] || '';
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
* ==================
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue