initial commit

This commit is contained in:
2025-11-25 12:27:53 +03:30
commit f9d16ab078
102 changed files with 11156 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
from . import hooks
from . import models
def post_init(cr, registry):
"""Post install hook proxy for older Odoo hooks invocation style."""
hooks.post_init(cr, registry)

View File

@@ -0,0 +1,18 @@
{
"name": "Project HTML editor + RTL",
"version": "1.0.0",
"summary": "Enable HTML editor for project task descriptions and notes, and force RTL when needed",
"description": "This small addon configures project task description fields to use the HTML editor and injects a small JS to enable RTL rendering in the editor on project task forms.",
"category": "Project",
"author": "Automated",
"depends": ["project", "html_editor"],
"license": "LGPL-3",
"data": [
"views/project_task_view.xml",
],
"installable": True,
"application": False,
"assets": {
"web.assets_backend": ["project_html_rtl/static/src/js/html_field_patch.js"],
},
}

View File

@@ -0,0 +1,5 @@
def post_init(cr, registry):
# Lightweight post-init: nothing heavy here. This placeholder can be
# extended later if database changes are desired at install time.
# Keep this simple to avoid unexpected side-effects.
print("project_html_rtl: post_init hook called")

View File

@@ -0,0 +1 @@
from . import project_task

View File

@@ -0,0 +1,11 @@
from odoo import models, fields
class ProjectTask(models.Model):
_inherit = "project.task"
rtl_enable = fields.Boolean(
string="Force RTL in HTML editor",
help="When set, the HTML editor on this task will render in right-to-left direction",
default=False,
)

View File

@@ -0,0 +1,178 @@
/**
* Small script to force RTL direction on HTML editors inside project task forms.
* It finds HTML editor fields added by our view extension and sets dir="rtl" on
* the editable area so the editor displays in right-to-left mode.
*/
(function () {
'use strict';
function setEditorRTL() {
// Target editable areas that we marked via the view (data-project-html="1")
var elems = document.querySelectorAll('[data-project-html="1"]');
elems.forEach(function (el) {
var editable = el.querySelector('[contenteditable]');
var shouldRTL = false;
// Try to detect a nearby boolean field named rtl_enable in the same form
var form = el.closest('form');
if (form) {
// Look for a checkbox input (classic boolean) by name
var cb = form.querySelector('input[name="rtl_enable"]');
if (cb) {
shouldRTL = cb.checked;
} else {
// Fallback: look for any element with data-field="rtl_enable"
var df = form.querySelector('[data-field="rtl_enable"]');
if (df) {
// Try to find checkbox inside
var inner = df.querySelector('input[type="checkbox"]');
if (inner) shouldRTL = inner.checked;
}
}
}
if (editable) {
if (shouldRTL) {
editable.setAttribute('dir', 'rtl');
editable.style.direction = 'rtl';
} else {
editable.removeAttribute('dir');
editable.style.direction = '';
}
}
});
}
// Run after DOM ready and also on Odoo bus events when views are re-rendered.
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () {
setEditorRTL();
// start observer after body exists
startObserver();
});
} else {
setEditorRTL();
startObserver();
}
function startObserver() {
// Odoo may dynamically rerender; observe mutations under the document body
// and re-apply the RTL attribute when new nodes appear.
try {
if (!document.body) return;
var observer = new MutationObserver(function () {
setEditorRTL();
});
observer.observe(document.body, { childList: true, subtree: true });
} catch (e) {
// Fail silently — we don't want to break the client if MutationObserver isn't available
console.warn('project_html_rtl: could not start MutationObserver', e && e.message);
}
}
})();
/**
* Small script to force RTL direction on HTML editors inside project task forms.
* It finds HTML editor fields added by our view extension and sets dir="rtl" on
* the editable area so the editor displays in right-to-left mode.
*/
(function () {
'use strict';
function setEditorRTL() {
// Target editable areas that we marked via the view (data-project-html="1")
var elems = document.querySelectorAll('[data-project-html="1"]');
elems.forEach(function (el) {
var editable = el.querySelector('[contenteditable]');
var shouldRTL = false;
// Try to detect a nearby boolean field named rtl_enable in the same form
var form = el.closest('form');
if (form) {
// Look for a checkbox input (classic boolean) by name
var cb = form.querySelector('input[name="rtl_enable"]');
if (cb) {
shouldRTL = cb.checked;
} else {
// Fallback: look for any element with data-field="rtl_enable"
var df = form.querySelector('[data-field="rtl_enable"]');
if (df) {
// Try to find checkbox inside
var inner = df.querySelector('input[type="checkbox"]');
if (inner) shouldRTL = inner.checked;
}
}
}
if (editable) {
if (shouldRTL) {
editable.setAttribute('dir', 'rtl');
editable.style.direction = 'rtl';
/**
* Small script to force RTL direction on HTML editors inside project task forms.
* It finds HTML editor fields added by our view extension and sets dir="rtl" on
* the editable area so the editor displays in right-to-left mode.
*/
(function () {
'use strict';
function setEditorRTL() {
// Target editable areas that we marked via the view (data-project-html="1")
var elems = document.querySelectorAll('[data-project-html="1"]');
elems.forEach(function (el) {
var editable = el.querySelector('[contenteditable]');
var shouldRTL = false;
// Try to detect a nearby boolean field named rtl_enable in the same form
var form = el.closest('form');
if (form) {
// Look for a checkbox input (classic boolean) by name
var cb = form.querySelector('input[name="rtl_enable"]');
if (cb) {
shouldRTL = cb.checked;
} else {
// Fallback: look for any element with data-field="rtl_enable"
var df = form.querySelector('[data-field="rtl_enable"]');
if (df) {
// Try to find checkbox inside
var inner = df.querySelector('input[type="checkbox"]');
if (inner) shouldRTL = inner.checked;
}
}
}
if (editable) {
if (shouldRTL) {
editable.setAttribute('dir', 'rtl');
editable.style.direction = 'rtl';
} else {
editable.removeAttribute('dir');
editable.style.direction = '';
}
}
});
}
// Run after DOM ready and also on Odoo bus events when views are re-rendered.
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () {
setEditorRTL();
// start observer after body exists
startObserver();
});
} else {
setEditorRTL();
startObserver();
}
function startObserver() {
// Odoo may dynamically rerender; observe mutations under the document body
// and re-apply the RTL attribute when new nodes appear.
try {
if (!document.body) return;
var observer = new MutationObserver(function () {
setEditorRTL();
});
observer.observe(document.body, { childList: true, subtree: true });
} catch (e) {
// Fail silently — we don't want to break the client if MutationObserver isn't available
console.warn('project_html_rtl: could not start MutationObserver', e && e.message);
}
}
})();
} else {

View File

@@ -0,0 +1,132 @@
/**
* editor_rtl_v2.js — safer version of the RTL injector for the HTML editor.
*/
(function () {
'use strict';
var MANAGED_FLAG = 'data-project-html-rtl-managed';
var IFRAME_LISTENER_FLAG = 'data-project-html-rtl-listener';
var TARGET_FIELDS = ['description', 'notes'];
function findRtlCheckbox(form) {
if (!form) {
return null;
}
var direct = form.querySelector('input[name="rtl_enable"]');
if (direct) {
return direct;
}
var wrapper = form.querySelector('[data-field="rtl_enable"]');
if (!wrapper) {
return null;
}
return wrapper.querySelector('input[type="checkbox"]');
}
function eachTargetField(callback) {
TARGET_FIELDS.forEach(function (fieldName) {
var containers = document.querySelectorAll('[data-field="' + fieldName + '"]');
containers.forEach(function (container) {
callback(container, fieldName);
});
});
}
function getEditableFromContainer(container) {
if (!container) {
return null;
}
// Try the standard inline editable div first
var editable = container.querySelector('.odoo-editor-editable');
if (editable) {
return editable;
}
// Older/editor fallback: any contenteditable descendant
editable = container.querySelector('[contenteditable="true"]');
if (editable) {
return editable;
}
// Some variants render inside an iframe
var iframe = container.querySelector('iframe');
if (iframe) {
if (!iframe.hasAttribute(IFRAME_LISTENER_FLAG)) {
iframe.setAttribute(IFRAME_LISTENER_FLAG, '1');
iframe.addEventListener('load', function () {
setEditorRTL();
});
}
try {
if (iframe.contentDocument && iframe.contentDocument.body) {
return iframe.contentDocument.body;
}
} catch (err) {
console.warn('project_html_rtl v2: unable to access iframe body', err && err.message);
}
}
return null;
}
function setEditorRTL() {
eachTargetField(function (container) {
try {
var editable = getEditableFromContainer(container);
if (!editable) {
return;
}
var form = container.closest('form');
var cb = findRtlCheckbox(form);
var shouldRTL = cb ? cb.checked : false;
var isManaged = editable.hasAttribute(MANAGED_FLAG);
if (shouldRTL) {
editable.setAttribute('dir', 'rtl');
editable.style.direction = 'rtl';
if (!isManaged) {
editable.setAttribute(MANAGED_FLAG, '1');
}
} else if (isManaged) {
editable.removeAttribute('dir');
editable.style.direction = '';
editable.removeAttribute(MANAGED_FLAG);
}
} catch (e) {
// Don't break the app if something unexpected happens
console.warn('project_html_rtl v2: error applying RTL', e && e.message);
}
});
}
function startObserver() {
try {
if (!document.body) return;
var observer = new MutationObserver(function () {
setEditorRTL();
});
observer.observe(document.body, { childList: true, subtree: true });
} catch (e) {
console.warn('project_html_rtl v2: could not start MutationObserver', e && e.message);
}
}
document.addEventListener('change', function (ev) {
var target = ev.target;
if (target && target.name === 'rtl_enable') {
setEditorRTL();
}
});
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () {
setEditorRTL();
startObserver();
});
} else {
setEditorRTL();
startObserver();
}
})();

View File

@@ -0,0 +1,87 @@
odoo.define('project_html_rtl.html_field_patch', [], function (require) {
'use strict';
const { patch } = require('@web/core/utils/patch');
const { useEffect } = require('@odoo/owl');
const { HtmlField } = require('@html_editor/fields/html_field');
// Capture original methods so we can call them from our patch.
const origSetup = HtmlField.prototype.setup;
const origOnEditorLoad = HtmlField.prototype.onEditorLoad;
const origOnBlur = HtmlField.prototype.onBlur;
const TARGET_MODEL = 'project.task';
const TARGET_FIELDS = new Set(['description', 'notes']);
const MANAGED_FLAG = 'data-project-html-rtl-managed';
function isTarget(component) {
return (
component.props?.record?.resModel === TARGET_MODEL &&
TARGET_FIELDS.has(component.props?.name)
);
}
function applyDirection(component) {
if (!isTarget(component) || component.state?.showCodeView) {
return;
}
const editor = component.editor;
if (!editor || !editor.editable) {
return;
}
const shouldRtl = !!component.props.record?.data?.rtl_enable;
const editable = editor.editable;
const isManaged = editable.hasAttribute(MANAGED_FLAG);
if (shouldRtl) {
editable.setAttribute('dir', 'rtl');
editable.style.direction = 'rtl';
if (!isManaged) {
editable.setAttribute(MANAGED_FLAG, '1');
}
} else if (isManaged) {
editable.removeAttribute('dir');
editable.style.direction = '';
editable.removeAttribute(MANAGED_FLAG);
}
}
patch(
HtmlField.prototype,
{
setup() {
// Call the original setup implementation if present
if (typeof origSetup === 'function') {
origSetup.apply(this, arguments);
}
if (!isTarget(this)) {
return;
}
useEffect(
() => {
applyDirection(this);
},
() => [this.props.record?.data?.rtl_enable, this.state?.showCodeView]
);
},
onEditorLoad() {
if (typeof origOnEditorLoad === 'function') {
origOnEditorLoad.apply(this, arguments);
}
if (isTarget(this)) {
applyDirection(this);
}
},
onBlur() {
const result = typeof origOnBlur === 'function' ? origOnBlur.apply(this, arguments) : undefined;
if (isTarget(this)) {
applyDirection(this);
}
return result;
},
},
'project_html_rtl'
);
});

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="project_task_form_inherit_html" model="ir.ui.view">
<field name="name">project.task.form.inherit.html</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="project.view_task_form2"/>
<field name="arch" type="xml">
<!-- Enable HTML widget on description and notes when present; add a marker attribute so JS can target them. -->
<xpath expr="//field[@name='description']|//field[@name='notes']" position="attributes">
<attribute name="widget">html</attribute>
<attribute name="data-project-html">1</attribute>
</xpath>
<!-- Add per-task RTL toggle to the form (inside the first sheet) -->
<xpath expr="//form//sheet" position="inside">
<group>
<field name="rtl_enable"/>
</group>
</xpath>
</field>
</record>
</odoo>