hoàn thành block editor của Pages
This commit is contained in:
@@ -1,4 +1,40 @@
|
||||
/* Custom CSS - This file has the highest priority and will override other styles */
|
||||
|
||||
/* ==========================================================================
|
||||
Design System Tokens (Project-wide CSS Variables)
|
||||
========================================================================== */
|
||||
:root {
|
||||
/* Primary brand colors */
|
||||
--color-primary: #007bff;
|
||||
--color-primary-hover: #0056b3;
|
||||
--color-primary-light: #f0f9ff;
|
||||
|
||||
/* Secondary & Accent colors */
|
||||
--color-secondary: #6c757d;
|
||||
--color-success: #28a745;
|
||||
--color-warning: #ffc107;
|
||||
--color-danger: #dc3545;
|
||||
|
||||
/* Neutrals */
|
||||
--color-background: #ffffff;
|
||||
--color-surface: #fafafa;
|
||||
--color-border: #dee2e6;
|
||||
|
||||
/* Text colors */
|
||||
--color-text-main: #333333;
|
||||
--color-text-muted: #6c757d;
|
||||
--color-text-light: #ffffff;
|
||||
|
||||
/* Typography */
|
||||
--font-family-base: 'Open Sans', 'Inter', sans-serif;
|
||||
--font-family-heading: 'Lora', 'Georgia', serif;
|
||||
|
||||
/* Spacing & Radii */
|
||||
--spacing-md: 20px;
|
||||
--radius-md: 4px;
|
||||
--radius-lg: 8px;
|
||||
}
|
||||
|
||||
[data-component-id="umass_base:tophat"] {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* SIS Vietnam - Block Editor Configuration
|
||||
*
|
||||
* Built on Editor.js (https://editorjs.io)
|
||||
*
|
||||
* PLUGIN ARCHITECTURE:
|
||||
* =====================
|
||||
* This editor uses a plugin registry pattern. The built-in block types
|
||||
* (Header, List, Quote, etc.) are registered by default.
|
||||
*
|
||||
* To add a CUSTOM BLOCK TYPE in the future:
|
||||
*
|
||||
* 1. Create a new JS file in /js/manage/editor-plugins/
|
||||
* Example: /js/manage/editor-plugins/my-custom-block.js
|
||||
*
|
||||
* 2. In that file, register your tool BEFORE the editor initializes:
|
||||
*
|
||||
* window.SISEditorPlugins = window.SISEditorPlugins || {};
|
||||
* window.SISEditorPlugins['myBlock'] = {
|
||||
* class: MyBlockClass, // Your Editor.js Tool class
|
||||
* inlineToolbar: true, // optional
|
||||
* config: { ... } // optional tool-specific config
|
||||
* };
|
||||
*
|
||||
* 3. Load the script in your template BEFORE editor-config.js:
|
||||
* <script src="/js/manage/editor-plugins/my-custom-block.js"></script>
|
||||
*
|
||||
* 4. The editor will automatically pick up all registered plugins.
|
||||
*/
|
||||
|
||||
// Global plugin registry — external plugins register here
|
||||
window.SISEditorPlugins = window.SISEditorPlugins || {};
|
||||
|
||||
/**
|
||||
* Initialize the SIS Block Editor on a given holder element.
|
||||
*
|
||||
* @param {string} holderId - The DOM element ID for the editor container
|
||||
* @param {string} hiddenInputId - The DOM element ID for the hidden input storing JSON
|
||||
* @param {object|null} initialData - Pre-existing Editor.js JSON data to load
|
||||
* @returns {EditorJS} The editor instance
|
||||
*/
|
||||
function initSISEditor(holderId, hiddenInputId, initialData) {
|
||||
'use strict';
|
||||
|
||||
// === Built-in Tools ===
|
||||
var builtInTools = {
|
||||
header: {
|
||||
class: Header,
|
||||
inlineToolbar: true,
|
||||
config: {
|
||||
placeholder: 'Enter a heading...',
|
||||
levels: [2, 3, 4],
|
||||
defaultLevel: 2
|
||||
}
|
||||
},
|
||||
list: {
|
||||
class: NestedList,
|
||||
inlineToolbar: true,
|
||||
config: {
|
||||
defaultStyle: 'unordered'
|
||||
}
|
||||
},
|
||||
quote: {
|
||||
class: Quote,
|
||||
inlineToolbar: true,
|
||||
config: {
|
||||
quotePlaceholder: 'Enter a quote...',
|
||||
captionPlaceholder: 'Quote author'
|
||||
}
|
||||
},
|
||||
delimiter: {
|
||||
class: Delimiter
|
||||
},
|
||||
table: {
|
||||
class: Table,
|
||||
inlineToolbar: true,
|
||||
config: {
|
||||
rows: 2,
|
||||
cols: 3
|
||||
}
|
||||
},
|
||||
code: {
|
||||
class: CodeTool
|
||||
},
|
||||
warning: {
|
||||
class: Warning,
|
||||
inlineToolbar: true,
|
||||
config: {
|
||||
titlePlaceholder: 'Title',
|
||||
messagePlaceholder: 'Message'
|
||||
}
|
||||
},
|
||||
marker: {
|
||||
class: Marker
|
||||
},
|
||||
inlineCode: {
|
||||
class: InlineCode
|
||||
},
|
||||
underline: {
|
||||
class: Underline
|
||||
},
|
||||
image: {
|
||||
class: ImageTool,
|
||||
config: {
|
||||
endpoints: {
|
||||
byFile: '/api/manage/media/upload',
|
||||
},
|
||||
field: 'file',
|
||||
types: 'image/*'
|
||||
}
|
||||
},
|
||||
attaches: {
|
||||
class: AttachesTool,
|
||||
config: {
|
||||
endpoint: '/api/manage/media/upload',
|
||||
field: 'file'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// === Merge built-in tools with any registered plugins ===
|
||||
var allTools = Object.assign({}, builtInTools, window.SISEditorPlugins);
|
||||
|
||||
// === Parse initial data ===
|
||||
var editorData = null;
|
||||
if (initialData && typeof initialData === 'string') {
|
||||
try {
|
||||
editorData = JSON.parse(initialData);
|
||||
} catch (e) {
|
||||
console.warn('[SIS Editor] Could not parse initial data as JSON, starting empty.', e);
|
||||
editorData = null;
|
||||
}
|
||||
} else if (initialData && typeof initialData === 'object') {
|
||||
editorData = initialData;
|
||||
}
|
||||
|
||||
// === Create the Editor ===
|
||||
var editor = new EditorJS({
|
||||
holder: holderId,
|
||||
tools: allTools,
|
||||
data: editorData || undefined,
|
||||
placeholder: 'Click here to start writing your page content...',
|
||||
autofocus: false,
|
||||
onReady: function() {
|
||||
console.log('[SIS Editor] Ready. Tools loaded:', Object.keys(allTools));
|
||||
},
|
||||
onChange: function(api, event) {
|
||||
// Auto-save to hidden input on every change
|
||||
api.saver.save().then(function(outputData) {
|
||||
var hiddenInput = document.getElementById(hiddenInputId);
|
||||
if (hiddenInput) {
|
||||
hiddenInput.value = JSON.stringify(outputData);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// === Form submission handler ===
|
||||
// Ensure the latest content is saved before form submit
|
||||
var form = document.querySelector('form');
|
||||
if (form) {
|
||||
var submitHandler = function(event) {
|
||||
event.preventDefault();
|
||||
editor.save().then(function(outputData) {
|
||||
var hiddenInput = document.getElementById(hiddenInputId);
|
||||
if (hiddenInput) {
|
||||
hiddenInput.value = JSON.stringify(outputData);
|
||||
}
|
||||
// Now submit the form
|
||||
form.removeEventListener('submit', submitHandler);
|
||||
form.submit();
|
||||
}).catch(function(error) {
|
||||
console.error('[SIS Editor] Save failed:', error);
|
||||
});
|
||||
};
|
||||
form.addEventListener('submit', submitHandler);
|
||||
}
|
||||
|
||||
return editor;
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* SAMPLE PLUGIN — How to create a custom Editor.js block for SIS Vietnam
|
||||
*
|
||||
* This file demonstrates the plugin pattern. Copy this file and modify it
|
||||
* to create your own custom block types.
|
||||
*
|
||||
* STEPS:
|
||||
* 1. Copy this file and rename it (e.g., "my-video-block.js")
|
||||
* 2. Create your Tool class following the Editor.js API
|
||||
* 3. Register it in window.SISEditorPlugins
|
||||
* 4. Load it in your template <script> tag BEFORE editor-config.js
|
||||
*
|
||||
* DOCUMENTATION: https://editorjs.io/creating-a-block-tool/
|
||||
*/
|
||||
|
||||
// === Example: A simple "Alert Box" block ===
|
||||
|
||||
/*
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Define your block tool class
|
||||
class AlertBox {
|
||||
static get toolbox() {
|
||||
return {
|
||||
title: 'Alert Box',
|
||||
icon: '<svg width="17" height="15" viewBox="0 0 336 276"><path d="M291 36l-15-26a17 17 0 0 0-30 0L15 277h306L291 36z"/></svg>'
|
||||
};
|
||||
}
|
||||
|
||||
constructor({ data }) {
|
||||
this.data = data || {};
|
||||
}
|
||||
|
||||
render() {
|
||||
var wrapper = document.createElement('div');
|
||||
wrapper.style.padding = '12px';
|
||||
wrapper.style.border = '2px solid #f0ad4e';
|
||||
wrapper.style.borderRadius = '4px';
|
||||
wrapper.style.backgroundColor = '#fcf8e3';
|
||||
wrapper.contentEditable = true;
|
||||
wrapper.innerHTML = this.data.text || '';
|
||||
wrapper.addEventListener('input', function() {
|
||||
this.data.text = wrapper.innerHTML;
|
||||
}.bind(this));
|
||||
this.wrapper = wrapper;
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
save(blockContent) {
|
||||
return {
|
||||
text: blockContent.innerHTML
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Register the plugin — this is the KEY step
|
||||
window.SISEditorPlugins = window.SISEditorPlugins || {};
|
||||
window.SISEditorPlugins['alertBox'] = {
|
||||
class: AlertBox,
|
||||
inlineToolbar: true
|
||||
};
|
||||
|
||||
})();
|
||||
*/
|
||||
|
||||
// This file is intentionally commented out.
|
||||
// Uncomment the code above to enable the Alert Box block,
|
||||
// or use it as a template for your own custom blocks.
|
||||
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* Custom Editor.js Block Tool for embedding predefined HTML snippets.
|
||||
*/
|
||||
class HtmlSnippetTool {
|
||||
static get toolbox() {
|
||||
return {
|
||||
title: 'HTML Snippet',
|
||||
icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline></svg>'
|
||||
};
|
||||
}
|
||||
|
||||
constructor({ data, api }) {
|
||||
this.data = {
|
||||
id: data.id || ''
|
||||
};
|
||||
this.api = api;
|
||||
this.wrapper = undefined;
|
||||
}
|
||||
|
||||
render() {
|
||||
this.wrapper = document.createElement('div');
|
||||
this.wrapper.classList.add('ce-snippet-wrapper');
|
||||
this.wrapper.style.border = '1px solid #ddd';
|
||||
this.wrapper.style.padding = '15px';
|
||||
this.wrapper.style.borderRadius = '5px';
|
||||
this.wrapper.style.background = '#fafafa';
|
||||
|
||||
if (this.data.id) {
|
||||
this._showPreview(this.data.id);
|
||||
} else {
|
||||
this._showInput();
|
||||
}
|
||||
|
||||
return this.wrapper;
|
||||
}
|
||||
|
||||
_showInput() {
|
||||
this.wrapper.innerHTML = '';
|
||||
|
||||
const title = document.createElement('h4');
|
||||
title.style.marginTop = '0';
|
||||
title.style.marginBottom = '10px';
|
||||
title.innerText = 'Insert Predefined HTML Snippet';
|
||||
|
||||
const inputContainer = document.createElement('div');
|
||||
inputContainer.style.display = 'flex';
|
||||
inputContainer.style.gap = '10px';
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.classList.add('ce-input');
|
||||
input.placeholder = 'Enter Snippet ID (e.g., test_banner)';
|
||||
input.value = this.data.id;
|
||||
|
||||
const loadBtn = document.createElement('button');
|
||||
loadBtn.innerText = 'Load Preview';
|
||||
loadBtn.style.padding = '5px 15px';
|
||||
loadBtn.style.cursor = 'pointer';
|
||||
|
||||
loadBtn.addEventListener('click', () => {
|
||||
if (input.value.trim()) {
|
||||
this._showPreview(input.value.trim());
|
||||
}
|
||||
});
|
||||
|
||||
inputContainer.appendChild(input);
|
||||
inputContainer.appendChild(loadBtn);
|
||||
|
||||
this.wrapper.appendChild(title);
|
||||
this.wrapper.appendChild(inputContainer);
|
||||
}
|
||||
|
||||
_showPreview(snippetId) {
|
||||
this.wrapper.innerHTML = '<div style="color: #666;">Loading preview...</div>';
|
||||
|
||||
fetch('/api/manage/snippets/' + encodeURIComponent(snippetId))
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error('Snippet not found');
|
||||
return response.text();
|
||||
})
|
||||
.then(html => {
|
||||
this.data.id = snippetId;
|
||||
|
||||
this.wrapper.innerHTML = '';
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.style.display = 'flex';
|
||||
header.style.justifyContent = 'space-between';
|
||||
header.style.alignItems = 'center';
|
||||
header.style.marginBottom = '10px';
|
||||
header.style.borderBottom = '1px solid #ddd';
|
||||
header.style.paddingBottom = '5px';
|
||||
|
||||
const title = document.createElement('strong');
|
||||
title.innerText = 'Snippet: ' + snippetId;
|
||||
|
||||
const editBtn = document.createElement('button');
|
||||
editBtn.innerText = 'Edit ID';
|
||||
editBtn.style.fontSize = '12px';
|
||||
editBtn.style.cursor = 'pointer';
|
||||
editBtn.addEventListener('click', () => {
|
||||
this._showInput();
|
||||
});
|
||||
|
||||
header.appendChild(title);
|
||||
header.appendChild(editBtn);
|
||||
|
||||
const previewArea = document.createElement('div');
|
||||
previewArea.innerHTML = html;
|
||||
|
||||
// Prevent interactions inside preview from submitting forms or acting up
|
||||
previewArea.style.pointerEvents = 'none';
|
||||
|
||||
this.wrapper.appendChild(header);
|
||||
this.wrapper.appendChild(previewArea);
|
||||
})
|
||||
.catch(error => {
|
||||
this.wrapper.innerHTML = '<div style="color: red;">Error: ' + error.message + '</div>';
|
||||
const backBtn = document.createElement('button');
|
||||
backBtn.innerText = 'Try Again';
|
||||
backBtn.style.marginTop = '10px';
|
||||
backBtn.style.cursor = 'pointer';
|
||||
backBtn.addEventListener('click', () => this._showInput());
|
||||
this.wrapper.appendChild(backBtn);
|
||||
});
|
||||
}
|
||||
|
||||
save(blockContent) {
|
||||
return {
|
||||
id: this.data.id
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Register the plugin globally
|
||||
window.SISEditorPlugins = window.SISEditorPlugins || {};
|
||||
window.SISEditorPlugins['snippet'] = {
|
||||
class: HtmlSnippetTool
|
||||
};
|
||||
Reference in New Issue
Block a user