refactor: rename third-party JS libs folder from vendor to lib
Rename static/shared/js/vendor/ to static/shared/js/lib/ to avoid confusion with the app's "vendor" (seller) dashboard code in static/vendor/js/. - Rename directory: vendor/ → lib/ - Update all template references to use new path - Update CDN fallback documentation - Fix .gitignore to use /lib/ (root only) instead of lib/ (everywhere) Third-party libraries: - alpine.min.js - chart.umd.min.js - flatpickr.min.js - quill.js - tom-select.complete.min.js Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -40,7 +40,7 @@
|
||||
script.onerror = function() {
|
||||
console.warn('Chart.js CDN failed, loading local copy...');
|
||||
var fallbackScript = document.createElement('script');
|
||||
fallbackScript.src = '{{ url_for("static", path="shared/js/vendor/chart.umd.min.js") }}';
|
||||
fallbackScript.src = '{{ url_for("static", path="shared/js/lib/chart.umd.min.js") }}';
|
||||
document.head.appendChild(fallbackScript);
|
||||
};
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
script.onerror = function() {
|
||||
console.warn('Flatpickr CDN failed, loading local copy...');
|
||||
var fallbackScript = document.createElement('script');
|
||||
fallbackScript.src = '{{ url_for("static", path="shared/js/vendor/flatpickr.min.js") }}';
|
||||
fallbackScript.src = '{{ url_for("static", path="shared/js/lib/flatpickr.min.js") }}';
|
||||
document.head.appendChild(fallbackScript);
|
||||
};
|
||||
|
||||
|
||||
326
app/templates/shared/macros/richtext.html
Normal file
326
app/templates/shared/macros/richtext.html
Normal file
@@ -0,0 +1,326 @@
|
||||
{#
|
||||
Rich Text Editor Macros (Quill)
|
||||
===============================
|
||||
Reusable rich text editor components using Quill.js with Alpine.js integration.
|
||||
|
||||
Setup (in your template):
|
||||
1. Import the macros:
|
||||
{% from 'shared/macros/richtext.html' import quill_css, quill_js, quill_editor %}
|
||||
|
||||
2. Add CSS block (in {% block quill_css %}):
|
||||
{{ quill_css() }}
|
||||
|
||||
3. Add JS block (in {% block quill_script %}):
|
||||
{{ quill_js() }}
|
||||
|
||||
4. Use the editor in your form:
|
||||
{{ quill_editor(
|
||||
id='content-editor',
|
||||
model='form.content',
|
||||
placeholder='Write your content here...'
|
||||
) }}
|
||||
|
||||
Note: Quill loads from CDN with local fallback for offline use.
|
||||
#}
|
||||
|
||||
|
||||
{#
|
||||
Quill CSS Block
|
||||
===============
|
||||
Include this in your {% block quill_css %} to load Quill styles.
|
||||
Includes dark mode support.
|
||||
#}
|
||||
{% macro quill_css() %}
|
||||
<!-- Quill Snow Theme CSS with CDN fallback -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/quill@2.0.2/dist/quill.snow.css"
|
||||
onerror="this.onerror=null; this.href='/static/shared/css/vendor/quill.snow.css';"
|
||||
/>
|
||||
<!-- Quill Dark Mode Overrides -->
|
||||
<style>
|
||||
/* Editor container styling */
|
||||
.quill-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.quill-container .ql-toolbar {
|
||||
border-top-left-radius: 0.5rem;
|
||||
border-top-right-radius: 0.5rem;
|
||||
}
|
||||
.quill-container .ql-container {
|
||||
border-bottom-left-radius: 0.5rem;
|
||||
border-bottom-right-radius: 0.5rem;
|
||||
min-height: 200px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Dark mode styles */
|
||||
.dark .ql-toolbar.ql-snow {
|
||||
background-color: rgb(55 65 81);
|
||||
border-color: rgb(75 85 99);
|
||||
}
|
||||
.dark .ql-container.ql-snow {
|
||||
background-color: rgb(55 65 81);
|
||||
border-color: rgb(75 85 99);
|
||||
}
|
||||
.dark .ql-editor {
|
||||
color: rgb(209 213 219);
|
||||
}
|
||||
.dark .ql-editor.ql-blank::before {
|
||||
color: rgb(156 163 175);
|
||||
}
|
||||
.dark .ql-snow .ql-stroke {
|
||||
stroke: rgb(209 213 219);
|
||||
}
|
||||
.dark .ql-snow .ql-fill {
|
||||
fill: rgb(209 213 219);
|
||||
}
|
||||
.dark .ql-snow .ql-picker {
|
||||
color: rgb(209 213 219);
|
||||
}
|
||||
.dark .ql-snow .ql-picker-label {
|
||||
color: rgb(209 213 219);
|
||||
}
|
||||
.dark .ql-snow .ql-picker-options {
|
||||
background-color: rgb(55 65 81);
|
||||
border-color: rgb(75 85 99);
|
||||
}
|
||||
.dark .ql-snow .ql-picker-item {
|
||||
color: rgb(209 213 219);
|
||||
}
|
||||
.dark .ql-snow .ql-picker-item:hover {
|
||||
color: rgb(147 51 234);
|
||||
}
|
||||
.dark .ql-snow .ql-picker-item.ql-selected {
|
||||
color: rgb(147 51 234);
|
||||
}
|
||||
.dark .ql-toolbar.ql-snow .ql-picker-label:hover,
|
||||
.dark .ql-toolbar.ql-snow .ql-picker-label.ql-active,
|
||||
.dark .ql-toolbar.ql-snow button:hover,
|
||||
.dark .ql-toolbar.ql-snow button.ql-active {
|
||||
color: rgb(147 51 234);
|
||||
}
|
||||
.dark .ql-toolbar.ql-snow button:hover .ql-stroke,
|
||||
.dark .ql-toolbar.ql-snow button.ql-active .ql-stroke {
|
||||
stroke: rgb(147 51 234);
|
||||
}
|
||||
.dark .ql-toolbar.ql-snow button:hover .ql-fill,
|
||||
.dark .ql-toolbar.ql-snow button.ql-active .ql-fill {
|
||||
fill: rgb(147 51 234);
|
||||
}
|
||||
.dark .ql-snow a {
|
||||
color: rgb(167 139 250);
|
||||
}
|
||||
|
||||
/* Focus state */
|
||||
.ql-container.ql-snow:focus-within {
|
||||
border-color: rgb(147 51 234);
|
||||
}
|
||||
.ql-toolbar.ql-snow:has(+ .ql-container:focus-within) {
|
||||
border-color: rgb(147 51 234);
|
||||
}
|
||||
|
||||
/* Code block styling */
|
||||
.dark .ql-snow .ql-editor pre.ql-syntax {
|
||||
background-color: rgb(31 41 55);
|
||||
color: rgb(209 213 219);
|
||||
}
|
||||
|
||||
/* Tooltip styling */
|
||||
.dark .ql-snow .ql-tooltip {
|
||||
background-color: rgb(55 65 81);
|
||||
border-color: rgb(75 85 99);
|
||||
color: rgb(209 213 219);
|
||||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.3);
|
||||
}
|
||||
.dark .ql-snow .ql-tooltip input[type=text] {
|
||||
background-color: rgb(31 41 55);
|
||||
border-color: rgb(75 85 99);
|
||||
color: rgb(209 213 219);
|
||||
}
|
||||
.dark .ql-snow .ql-tooltip a.ql-action::after,
|
||||
.dark .ql-snow .ql-tooltip a.ql-remove::before {
|
||||
color: rgb(167 139 250);
|
||||
}
|
||||
</style>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Quill JS Block
|
||||
==============
|
||||
Include this in your {% block quill_script %} to load Quill.
|
||||
Uses CDN with local fallback.
|
||||
#}
|
||||
{% macro quill_js() %}
|
||||
<script>
|
||||
(function() {
|
||||
// Check if Quill already loaded
|
||||
if (window.Quill) return;
|
||||
|
||||
var script = document.createElement('script');
|
||||
script.src = 'https://cdn.jsdelivr.net/npm/quill@2.0.2/dist/quill.js';
|
||||
script.onerror = function() {
|
||||
console.warn('Quill CDN failed, loading local copy...');
|
||||
var fallbackScript = document.createElement('script');
|
||||
fallbackScript.src = '/static/shared/js/lib/quill.js';
|
||||
document.head.appendChild(fallbackScript);
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
})();
|
||||
</script>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Quill Editor Component
|
||||
======================
|
||||
A rich text editor with Alpine.js two-way binding.
|
||||
|
||||
Parameters:
|
||||
- id: Unique ID for the editor (required)
|
||||
- model: Alpine.js model to bind HTML content to (required)
|
||||
- placeholder: Placeholder text (default: 'Write something...')
|
||||
- min_height: Minimum height of editor (default: '200px')
|
||||
- toolbar: Toolbar configuration - 'full', 'standard', 'minimal' (default: 'standard')
|
||||
- readonly: Alpine.js variable for readonly state (optional)
|
||||
- disabled: Alpine.js variable for disabled state (optional)
|
||||
- label: Label text (optional)
|
||||
- required: Whether field is required (default: false)
|
||||
- help_text: Help text below editor (optional)
|
||||
|
||||
Toolbar presets:
|
||||
- 'full': All formatting options
|
||||
- 'standard': Headers, bold, italic, lists, links, images (default)
|
||||
- 'minimal': Bold, italic, lists only
|
||||
#}
|
||||
{% macro quill_editor(
|
||||
id,
|
||||
model,
|
||||
placeholder='Write something...',
|
||||
min_height='200px',
|
||||
toolbar='standard',
|
||||
readonly=none,
|
||||
disabled=none,
|
||||
label=none,
|
||||
required=false,
|
||||
help_text=none
|
||||
) %}
|
||||
<div class="quill-wrapper" {% if disabled %}x-bind:class="{ 'opacity-50 pointer-events-none': {{ disabled }} }"{% endif %}>
|
||||
{% if label %}
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
{{ label }}{% if required %} <span class="text-red-500">*</span>{% endif %}
|
||||
</label>
|
||||
{% endif %}
|
||||
|
||||
<div class="quill-container">
|
||||
<div id="{{ id }}" style="min-height: {{ min_height }};"></div>
|
||||
</div>
|
||||
|
||||
{% if help_text %}
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ help_text }}</p>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
// Wait for Alpine and Quill to be ready
|
||||
function initEditor() {
|
||||
if (typeof Alpine === 'undefined' || typeof Quill === 'undefined') {
|
||||
setTimeout(initEditor, 50);
|
||||
return;
|
||||
}
|
||||
|
||||
const container = document.getElementById('{{ id }}');
|
||||
if (!container || container._quillInitialized) return;
|
||||
container._quillInitialized = true;
|
||||
|
||||
// Toolbar configurations
|
||||
const toolbars = {
|
||||
full: [
|
||||
[{'header': [1, 2, 3, 4, 5, 6, false]}],
|
||||
['bold', 'italic', 'underline', 'strike'],
|
||||
[{'color': []}, {'background': []}],
|
||||
[{'list': 'ordered'}, {'list': 'bullet'}],
|
||||
[{'indent': '-1'}, {'indent': '+1'}],
|
||||
[{'align': []}],
|
||||
['blockquote', 'code-block'],
|
||||
['link', 'image', 'video'],
|
||||
['clean']
|
||||
],
|
||||
standard: [
|
||||
[{'header': [1, 2, 3, false]}],
|
||||
['bold', 'italic', 'underline'],
|
||||
[{'list': 'ordered'}, {'list': 'bullet'}],
|
||||
['link', 'image'],
|
||||
['clean']
|
||||
],
|
||||
minimal: [
|
||||
['bold', 'italic'],
|
||||
[{'list': 'ordered'}, {'list': 'bullet'}],
|
||||
['link'],
|
||||
['clean']
|
||||
]
|
||||
};
|
||||
|
||||
const quill = new Quill(container, {
|
||||
theme: 'snow',
|
||||
placeholder: '{{ placeholder }}',
|
||||
readOnly: {{ 'true' if readonly else 'false' }},
|
||||
modules: {
|
||||
toolbar: toolbars['{{ toolbar }}'] || toolbars.standard
|
||||
}
|
||||
});
|
||||
|
||||
// Find the Alpine component scope
|
||||
const wrapper = container.closest('[x-data]');
|
||||
if (wrapper && wrapper._x_dataStack) {
|
||||
const data = wrapper._x_dataStack[0];
|
||||
|
||||
// Set initial content from model
|
||||
const getModelValue = () => {
|
||||
try {
|
||||
return eval('data.{{ model }}');
|
||||
} catch(e) {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const setModelValue = (val) => {
|
||||
try {
|
||||
const parts = '{{ model }}'.split('.');
|
||||
let obj = data;
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
obj = obj[parts[i]];
|
||||
}
|
||||
obj[parts[parts.length - 1]] = val;
|
||||
} catch(e) {
|
||||
console.warn('Could not set model value:', e);
|
||||
}
|
||||
};
|
||||
|
||||
// Set initial content
|
||||
const initialContent = getModelValue();
|
||||
if (initialContent) {
|
||||
quill.root.innerHTML = initialContent;
|
||||
}
|
||||
|
||||
// Sync changes back to Alpine model
|
||||
quill.on('text-change', function() {
|
||||
const html = quill.root.innerHTML;
|
||||
const isEmpty = html === '<p><br></p>' || html === '<p></p>' || html === '';
|
||||
setModelValue(isEmpty ? '' : html);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Start initialization
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initEditor);
|
||||
} else {
|
||||
setTimeout(initEditor, 100);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
Reference in New Issue
Block a user