1
0
Fork 0
mirror of https://github.com/codex-team/codex.docs.git synced 2025-07-21 22:29:40 +02:00

added new logic

This commit is contained in:
exezzz 2025-05-26 18:47:04 +03:00
parent 8bcf23278d
commit 28580e300b
9 changed files with 500 additions and 549 deletions

View file

@ -1,5 +1,5 @@
# Stage 1 - build # Stage 1 - build
FROM node:16.14.0-alpine3.15 as build FROM node:18-alpine as build
## Install build toolchain, install node deps and compile native add-ons ## Install build toolchain, install node deps and compile native add-ons
RUN apk add --no-cache python3 make g++ git RUN apk add --no-cache python3 make g++ git
@ -8,9 +8,9 @@ WORKDIR /usr/src/app
COPY package.json yarn.lock ./ COPY package.json yarn.lock ./
RUN yarn install --production ##RUN yarn install --production
RUN cp -R node_modules prod_node_modules ##RUN cp -R node_modules prod_node_modules
RUN yarn install RUN yarn install
@ -19,12 +19,12 @@ COPY . .
RUN yarn build-all RUN yarn build-all
# Stage 2 - make final image # Stage 2 - make final image
FROM node:16.14.0-alpine3.15 FROM node:18-alpine
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY package.json yarn.lock ./ COPY package.json yarn.lock ./
COPY --from=build /usr/src/app/prod_node_modules ./node_modules COPY --from=build /usr/src/app/node_modules ./node_modules
COPY --from=build /usr/src/app/dist ./dist COPY --from=build /usr/src/app/dist ./dist
COPY --from=build /usr/src/app/public ./public COPY --from=build /usr/src/app/public ./public

View file

@ -25,6 +25,7 @@
"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,delimiter,list,link,image,table,inline-code,marker,warning,checklist,raw}@latest"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/abort-controller": "^3.374.0",
"@aws-sdk/client-s3": "^3.181.0", "@aws-sdk/client-s3": "^3.181.0",
"@codex-team/config-loader": "0.1.0-rc1", "@codex-team/config-loader": "0.1.0-rc1",
"@codexteam/shortcuts": "^1.2.0", "@codexteam/shortcuts": "^1.2.0",

View file

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

View file

@ -80,7 +80,7 @@ export default class Editor {
byUrl: '/api/transport/fetch' // Если нужна загрузка по URL byUrl: '/api/transport/fetch' // Если нужна загрузка по URL
} }
}, },
inlineToolbar: true shortcut: 'CMD+SHIFT+V'
}, },
image: { image: {

View file

@ -6,26 +6,33 @@ export default class VideoTool {
}; };
} }
constructor({ data, api, config }) { constructor({ data, api, config, block }) { // Äîáàâëÿåì block â ïàðàìåòðû
this.data = data || {}; this.data = data || {};
this.api = api; this.api = api;
this.config = config || {}; this.config = config || {};
this.block = block; // Ñîõðàíÿåì ññûëêó íà òåêóùèé áëîê
this.wrapper = null; this.wrapper = null;
this.alignment = data.alignment || 'center'; this.alignment = data.alignment || 'center';
this.width = data.width || '100%'; this.width = data.width || '100%';
this.filetype = data.filetype || 'file';
// Привязываем контекст для обработчиков // Ïðèâÿçûâàåì êîíòåêñò äëÿ îáðàáîò÷èêîâ
this._handleFileUpload = this._handleFileUpload.bind(this); this._handleFileUpload = this._handleFileUpload.bind(this);
this._initResize = this._initResize.bind(this); this._initResize = this._initResize.bind(this);
this._handleUrlSubmit = this._handleUrlSubmit.bind(this); // Added this binding this._handleUrlSubmit = this._handleUrlSubmit.bind(this);
} }
render() { render() {
this.wrapper = document.createElement('div'); this.wrapper = document.createElement('div');
this.wrapper.classList.add('video-tool'); this.wrapper.classList.add('video-tool');
if (this.data.url) { if (this.data.url) {
this._createVideoElement(this.data.url, this.data.caption || ''); // Äîáàâëÿåì ïðîâåðêó äëÿ ñîâìåñòèìîñòè ñî ñòàðûìè äàííûìè
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);
} else { } else {
this._createUploadForm(); this._createUploadForm();
} }
@ -34,49 +41,69 @@ export default class VideoTool {
} }
_createUploadForm() { _createUploadForm() {
const currentUrl = this.wrapper?.querySelector('.embed-url')?.value || '';
this.wrapper.innerHTML = ` this.wrapper.innerHTML = `
<div class="video-upload-tabs"> <div class="video-upload-tabs">
<div class="tabs-header"> <div class="tabs-header">
<button class="tab-button active" data-tab="upload">Upload</button> <button class="tab-button active" data-tab="upload">Upload</button>
<button class="tab-button" data-tab="embed">Embed URL</button> <button class="tab-button" data-tab="embed">Embed URL</button>
</div> </div>
<div class="tab-content active" data-tab="Загрузить с ПК"> <div class="tab-content active" data-tab="upload">
<label class="video-upload-button"> <label class="video-upload-button">
<input type="file" accept="video/mp4,video/webm,video/ogg" class="video-file-input"> <input type="file" accept="video/mp4,video/webm,video/ogg" class="video-file-input">
<span>Select Video File</span> <span>Select Video File</span>
</label> </label>
</div> </div>
<div class="tab-content" data-tab="Вставить ссылку"> <div class="tab-content" data-tab="embed">
<div class="embed-form"> <div class="embed-form">
<select class="embed-service"> <select class="embed-service">
<option value="youtube">YouTube</option> <option value="youtube">YouTube</option>
<option value="rutube">Rutube</option> <option value="rutube">Rutube</option>
</select> </select>
<input type="text" class="embed-url" placeholder="Paste video URL..."> <input type="text" class="embed-url" placeholder="Paste video URL...">
<button class="embed-submit">Вставить видео</button> <button class="embed-submit">Âñòàâèòü âèäåî</button>
</div> </div>
</div> </div>
</div> </div>
`; `;
// Tab switching
this._bindFormHandlers();
if (currentUrl) {
const urlInput = this.wrapper.querySelector('.embed-url');
if (urlInput) urlInput.value = currentUrl;
}
}
_bindFormHandlers() {
// Tab switching
this.wrapper.querySelectorAll('.tab-button').forEach(btn => { this.wrapper.querySelectorAll('.tab-button').forEach(btn => {
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
this.wrapper.querySelectorAll('.tab-button, .tab-content').forEach(el => { this.wrapper.querySelectorAll('.tab-button, .tab-content').forEach(el => {
el.classList.toggle('active', el.dataset.tab === btn.dataset.tab); el.classList.toggle('active', el.dataset.tab === btn.dataset.tab);
}); });
// Ïîñëå ïåðåêëþ÷åíèÿ âêëàäêè ïåðåïðèâÿçûâàåì îáðàáîò÷èêè
this._bindFormHandlers();
}); });
}); });
// File upload handler // File upload handler
this.wrapper.querySelector('.video-file-input') const fileInput = this.wrapper.querySelector('.video-file-input');
.addEventListener('change', this._handleFileUpload); if (fileInput) {
fileInput.addEventListener('change', this._handleFileUpload);
}
// URL embed handler // URL embed handler
this.wrapper.querySelector('.embed-submit') const embedSubmit = this.wrapper.querySelector('.embed-submit');
.addEventListener('click', this._handleUrlSubmit); if (embedSubmit) {
} embedSubmit.addEventListener('click', this._handleUrlSubmit);
}
}
async _handleFileUpload(event) { async _handleFileUpload(event) {
if (!event.target.files || event.target.files.length === 0) {
return;
}
const file = event.target.files[0]; const file = event.target.files[0];
if (!file) return; if (!file) return;
@ -89,106 +116,202 @@ export default class VideoTool {
const url = await this._uploadFile(file); const url = await this._uploadFile(file);
this.type = 'file'; this.type = 'file';
this.data = { url, type: 'file' }; this.data = { url, type: 'file' };
his._createVideoElement(url, ''); this._createVideoElement(url, '');
} catch (error) { } catch (error) {
this._showError('File upload failed: ' + error.message); this._showError('File upload failed: ' + error.message);
} }
} }
_handleUrlSubmit() { _handleUrlSubmit() {
const wrapper = this.wrapper; // Store reference to wrapper const wrapper = this.wrapper;
const service = wrapper.querySelector('.embed-service').value; const service = wrapper.querySelector('.embed-service').value;
const url = wrapper.querySelector('.embed-url').value.trim(); const urlInput = wrapper.querySelector('.embed-url');
const url = urlInput.value.trim();
if (!url) { if (!url) {
this._showError('Please enter a valid URL'); this._showError('Ïîæàëóéñòà, ââåäèòå URL âèäåî');
return; return;
} }
try { try {
const embedUrl = this._parseEmbedUrl(service, url); const submitBtn = wrapper.querySelector('.embed-submit');
this.type = service; submitBtn.disabled = true;
this.data = { url: embedUrl, type: service }; submitBtn.textContent = 'Îáðàáîòêà...';
this._createVideoElement(embedUrl, '');
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 = 'Âñòàâèòü âèäåî';
} catch (error) { } catch (error) {
this._showError('Invalid video URL: ' + error.message); this._showError(error.message);
const submitBtn = wrapper.querySelector('.embed-submit');
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.textContent = 'Âñòàâèòü âèäåî';
}
} }
} }
_parseEmbedUrl(service, url) { _parseEmbedUrl(service, url) {
// YouTube // YouTube
if (service === 'youtube') { if (service === 'youtube') {
const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/; // Ïîääåðæêà âñåõ ôîðìàòîâ YouTube ññûëîê
const match = url.match(regExp); const regExp = /(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/i;
if (match && match[2].length === 11) { const match = url.match(regExp);
return `https://www.youtube.com/embed/${match[2]}`; if (match && match[1]) {
} return `https://www.youtube.com/embed/${match[1]}`;
}
} }
// Rutube // Rutube
if (service === 'rutube') { if (service === 'rutube') {
const regExp = /rutube\.ru\/video\/([a-z0-9]+)/i; // Ïîääåðæêà âñåõ ôîðìàòîâ Rutube ññûëîê
const regExp = /rutube\.ru\/(?:video\/|play\/embed\/|video\/embed\/)?([a-zA-Z0-9]+)/i;
const match = url.match(regExp); const match = url.match(regExp);
if (match && match[1]) { if (match && match[1]) {
return `https://rutube.ru/play/embed/${match[1]}`; return `https://rutube.ru/play/embed/${match[1]}`;
} }
}
throw new Error('Invalid URL'); throw new Error('Íåâåðíûé URL Rutube. Ïðèìåð ïðàâèëüíîãî ôîðìàòà: https://rutube.ru/video/CODE/');
} }
_createVideoElement() { throw new Error('Invalid URL.');
this.wrapper.innerHTML = ''; }
const container = document.createElement('div'); _createVideoElement(url, caption, filetype = null) {
container.className = 'video-container'; this.wrapper.innerHTML = '';
container.style.margin = this._getAlignmentMargin();
container.style.width = this.width;
if (this.type === 'file') { 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'); const video = document.createElement('video');
video.src = this.data.url; video.src = url;
video.controls = true; video.controls = true;
video.style.position = 'absolute';
video.style.top = '0';
video.style.left = '0';
video.style.width = '100%'; video.style.width = '100%';
container.appendChild(video); video.style.height = '100%';
} mediaContainer.appendChild(video);
else if (this.type === 'youtube' || this.type === 'rutube') { } else {
const iframe = document.createElement('iframe'); const iframe = document.createElement('iframe');
iframe.src = this.data.url; iframe.src = url.includes('rutube.ru/video') ?
url.replace('rutube.ru/video', 'rutube.ru/play/embed') : url;
iframe.frameBorder = '0'; iframe.frameBorder = '0';
iframe.allowFullscreen = true; 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.width = '100%';
iframe.style.height = '400px'; iframe.style.height = '100%';
container.appendChild(iframe); mediaContainer.appendChild(iframe);
} }
this._addResizeHandle(container); container.appendChild(mediaContainer);
this._addCaption(container); this._addCaption(container, caption);
this.wrapper.appendChild(container); this._addResizeHandle(container);
this.wrapper.appendChild(container);
}
_addCaption(container, captionText) {
if (!captionText) return;
const caption = document.createElement('div');
caption.className = 'video-caption';
caption.contentEditable = true;
caption.innerHTML = captionText;
container.appendChild(caption);
} }
async _uploadFile(file) { async _uploadFile(file) {
const formData = new FormData(); try {
formData.append('file', file); const formData = new FormData();
formData.append('file', file);
const response = await fetch(this.config.uploader.byFile, { const response = await fetch(this.config.uploader.byFile, {
method: 'POST', method: 'POST',
body: formData body: formData
}); });
if (!response.ok) throw new Error('Upload failed'); if (!response.ok) {
const data = await response.json(); throw new Error(`HTTP error! status: ${response.status}`);
return data.file.url; }
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;
}
} }
_addResizeHandle() { _addResizeHandle(container) {
const container = this.wrapper.querySelector('.video-container'); const video = container.querySelector('video, iframe');
const video = container.querySelector('video');
if (!video) return; // Åñëè âèäåî íå íàéäåíî, âûõîäèì
const handle = document.createElement('div'); const handle = document.createElement('div');
handle.className = 'resize-handle'; handle.className = 'resize-handle';
handle.innerHTML = '↔'; handle.innerHTML = '-';
handle.addEventListener('mousedown', this._initResize);
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); container.appendChild(handle);
} }
@ -215,14 +338,17 @@ export default class VideoTool {
} }
save(blockContent) { save(blockContent) {
const video = blockContent.querySelector('video'); const video = blockContent.querySelector('video, iframe');
const caption = blockContent.querySelector('.video-caption'); const caption = blockContent.querySelector('.video-caption');
return { return {
url: video?.src || '', url: video?.src || this.data.url || '',
caption: caption?.innerHTML || '', caption: caption?.innerHTML || this.data.caption || '',
width: video?.style.width || '100%', width: this.width || '100%',
alignment: this.alignment alignment: this.alignment,
filetype: this.filetype || (video?.tagName === 'IFRAME' ?
(video.src.includes('youtube') ? 'youtube' : 'rutube')
: 'file')
}; };
} }
@ -235,6 +361,27 @@ export default class VideoTool {
} }
_showError(message) { _showError(message) {
this.wrapper.innerHTML = `<div class="video-error">${message}</div>`; 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>
</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

@ -112,6 +112,7 @@ export default class Writing {
*/ */
async getData() { async getData() {
const editorData = await this.editor.save(); const editorData = await this.editor.save();
console.log('Editor data:', JSON.stringify(editorData, null, 2));
const firstBlock = editorData.blocks.length ? editorData.blocks[0] : null; const firstBlock = editorData.blocks.length ? editorData.blocks[0] : null;
const title = firstBlock && firstBlock.type === 'header' ? firstBlock.data.text : null; const title = firstBlock && firstBlock.type === 'header' ? firstBlock.data.text : null;
let uri = ''; let uri = '';

View file

@ -495,17 +495,3 @@
} }
} }
} }
.block-embed {
margin: 0;
&__iframe {
width: 100%;
height: 450px;
border: 0;
@media (--mobile) {
height: 200px;
}
}
}

View file

@ -163,13 +163,19 @@
} }
.video-error { .video-error {
color: #ff0000; padding: 15px;
padding: 1rem; background: #ffebee;
background: #ffecec; border: 1px solid #ffcdd2;
border-radius: 4px; border-radius: 4px;
margin: 1rem 0; color: #c62828;
} }
.video-error p {
margin: 0 0 10px 0;
}
.color-palette-item { .color-palette-item {
width: 20px; width: 20px;
height: 20px; height: 20px;
@ -302,23 +308,18 @@
color: #666; color: #666;
} }
.video-error {
padding: 20px;
color: #d32f2f;
background: #ffebee;
border-radius: 4px;
}
.retry-button { .retry-button {
margin-top: 10px; background: #2196f3;
padding: 5px 10px; color: white;
background: #1976d2; border: none;
color: white; padding: 5px 10px;
border: none; border-radius: 3px;
border-radius: 4px; cursor: pointer;
cursor: pointer;
} }
.retry-button:hover {
background: #0d8aee;
}
.video-tool { .video-tool {
position: relative; position: relative;
margin: 1rem 0; margin: 1rem 0;
@ -373,8 +374,27 @@
} }
.block-video { .block-video {
margin: 1.5rem auto; position: relative;
max-width: 100%; padding-bottom: 56.25%; /* 16:9 */
height: 0;
overflow: hidden;
}
.block-video > div {
position: relative;
padding-bottom: 56.25%; /* 16:9 ñîîòíîøåíèå */
height: 0;
overflow: hidden;
background: #000;
}
.block-video iframe,
.block-video video {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
} }
.block-video__content { .block-video__content {
@ -542,7 +562,7 @@
cursor: pointer; cursor: pointer;
} }
.video-container iframe { .video-container video {
border: none; display: block;
min-height: 400px; margin: 0 auto;
} }

607
yarn.lock

File diff suppressed because it is too large Load diff