mirror of
https://github.com/odoo/runbot.git
synced 2025-03-15 23:45:44 +07:00
[IMP] runbot: add basic owl structure
Adds the minimal requirements for owl and xml file based templating for the frontend.
This commit is contained in:
parent
67407e3ec2
commit
0db0bfcf5b
@ -67,6 +67,16 @@
|
||||
'runbot/static/src/diff_match_patch_module.js',
|
||||
'runbot/static/src/fields/*',
|
||||
],
|
||||
'runbot.assets_frontend': [
|
||||
('include', 'web.assets_frontend_minimal'), # Pray the gods this stays named correctly
|
||||
|
||||
'runbot/static/libs/owl.js',
|
||||
'runbot/static/src/owl_module.js',
|
||||
|
||||
'runbot/static/src/vendored/**/*', # Vendored files coming from odoo modules
|
||||
|
||||
'runbot/static/src/frontend/root.js',
|
||||
]
|
||||
},
|
||||
'post_load': 'runbot_post_load',
|
||||
}
|
||||
|
64
runbot/static/src/frontend/root.js
Normal file
64
runbot/static/src/frontend/root.js
Normal file
@ -0,0 +1,64 @@
|
||||
import { whenReady, Component, xml, App, onError } from '@runbot/owl';
|
||||
|
||||
import { getTemplate } from '@web/core/templates';
|
||||
import { registry } from '@web/core/registry';
|
||||
import { useRegistry } from '@web/core/registry_hook';
|
||||
|
||||
|
||||
const mainComponents = registry.category('main.components');
|
||||
|
||||
class ErrorHandler extends Component {
|
||||
static template = xml`<t t-slot="default" />`;
|
||||
static props = ["onError", "slots"];
|
||||
setup() {
|
||||
onError((error) => {
|
||||
this.props.onError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ComponentContainer extends Component {
|
||||
static components = { ErrorHandler };
|
||||
static props = {};
|
||||
static template = xml`
|
||||
<div class="o-main-components-container">
|
||||
<t t-foreach="Components.entries" t-as="C" t-key="C[0]">
|
||||
<ErrorHandler onError="error => this.handleComponentError(error, C)">
|
||||
<t t-component="C[1].Component" t-props="C[1].props"/>
|
||||
</ErrorHandler>
|
||||
</t>
|
||||
</div>
|
||||
`;
|
||||
|
||||
setup() {
|
||||
this.Components = useRegistry(mainComponents);
|
||||
}
|
||||
|
||||
handleComponentError(error, C) {
|
||||
console.error('Error while rendering', C[0], 'removing from app.');
|
||||
// remove the faulty component and rerender without it
|
||||
this.Components.entries.splice(this.Components.entries.indexOf(C), 1);
|
||||
this.render();
|
||||
/**
|
||||
* we rethrow the error to notify the user something bad happened.
|
||||
* We do it after a tick to make sure owl can properly finish its
|
||||
* rendering
|
||||
*/
|
||||
Promise.resolve().then(() => {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap the frontend.
|
||||
*/
|
||||
(async function startApp() {
|
||||
await whenReady();
|
||||
|
||||
const app = new App(ComponentContainer, {
|
||||
getTemplate,
|
||||
env: {},
|
||||
});
|
||||
await app.mount(document.body);
|
||||
})();
|
212
runbot/static/src/vendored/registry.js
Normal file
212
runbot/static/src/vendored/registry.js
Normal file
@ -0,0 +1,212 @@
|
||||
/** @odoo-module alias=@web/core/registry default=false **/
|
||||
|
||||
|
||||
import { EventBus, validate } from "@runbot/owl";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Errors
|
||||
// -----------------------------------------------------------------------------
|
||||
export class KeyNotFoundError extends Error {}
|
||||
|
||||
export class DuplicatedKeyError extends Error {}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Validation
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const validateSchema = (name, key, value, schema) => {
|
||||
if (!odoo.debug) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
validate(value, schema);
|
||||
} catch (error) {
|
||||
throw new Error(`Validation error for key "${key}" in registry "${name}": ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Types
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @template S
|
||||
* @template C
|
||||
* @typedef {import("registries").RegistryData<S, C>} RegistryData
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {T extends RegistryData<any, any> ? T : RegistryData<T, {}>} ToRegistryData
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {ToRegistryData<T>["__itemShape"]} GetRegistryItemShape
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {ToRegistryData<T>["__categories"]} GetRegistryCategories
|
||||
*/
|
||||
|
||||
/**
|
||||
* Registry
|
||||
*
|
||||
* The Registry class is basically just a mapping from a string key to an object.
|
||||
* It is really not much more than an object. It is however useful for the
|
||||
* following reasons:
|
||||
*
|
||||
* 1. it let us react and execute code when someone add something to the registry
|
||||
* (for example, the FunctionRegistry subclass this for this purpose)
|
||||
* 2. it throws an error when the get operation fails
|
||||
* 3. it provides a chained API to add items to the registry.
|
||||
*
|
||||
* @template T
|
||||
*/
|
||||
export class Registry extends EventBus {
|
||||
/**
|
||||
* @param {string} [name]
|
||||
*/
|
||||
constructor(name) {
|
||||
super();
|
||||
/** @type {Record<string, [number, GetRegistryItemShape<T>]>}*/
|
||||
this.content = {};
|
||||
/** @type {{ [P in keyof GetRegistryCategories<T>]?: Registry<GetRegistryCategories<T>[P]> }} */
|
||||
this.subRegistries = {};
|
||||
/** @type {GetRegistryItemShape<T>[]}*/
|
||||
this.elements = null;
|
||||
/** @type {[string, GetRegistryItemShape<T>][]}*/
|
||||
this.entries = null;
|
||||
this.name = name;
|
||||
this.validationSchema = null;
|
||||
|
||||
this.addEventListener("UPDATE", () => {
|
||||
this.elements = null;
|
||||
this.entries = null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an entry (key, value) to the registry if key is not already used. If
|
||||
* the parameter force is set to true, an entry with same key (if any) is replaced.
|
||||
*
|
||||
* Note that this also returns the registry, so another add method call can
|
||||
* be chained
|
||||
*
|
||||
* @param {string} key
|
||||
* @param {GetRegistryItemShape<T>} value
|
||||
* @param {{force?: boolean, sequence?: number}} [options]
|
||||
* @returns {Registry<T>}
|
||||
*/
|
||||
add(key, value, { force, sequence } = {}) {
|
||||
if (this.validationSchema) {
|
||||
validateSchema(this.name, key, value, this.validationSchema);
|
||||
}
|
||||
if (!force && key in this.content) {
|
||||
throw new DuplicatedKeyError(
|
||||
`Cannot add key "${key}" in the "${this.name}" registry: it already exists`
|
||||
);
|
||||
}
|
||||
let previousSequence;
|
||||
if (force) {
|
||||
const elem = this.content[key];
|
||||
previousSequence = elem && elem[0];
|
||||
}
|
||||
sequence = sequence === undefined ? previousSequence || 50 : sequence;
|
||||
this.content[key] = [sequence, value];
|
||||
const payload = { operation: "add", key, value };
|
||||
this.trigger("UPDATE", payload);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an item from the registry
|
||||
*
|
||||
* @param {string} key
|
||||
* @returns {GetRegistryItemShape<T>}
|
||||
*/
|
||||
get(key, defaultValue) {
|
||||
if (arguments.length < 2 && !(key in this.content)) {
|
||||
throw new KeyNotFoundError(`Cannot find key "${key}" in the "${this.name}" registry`);
|
||||
}
|
||||
const info = this.content[key];
|
||||
return info ? info[1] : defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the presence of a key in the registry
|
||||
*
|
||||
* @param {string} key
|
||||
* @returns {boolean}
|
||||
*/
|
||||
contains(key) {
|
||||
return key in this.content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all elements in the registry. Note that it is ordered
|
||||
* according to the sequence numbers.
|
||||
*
|
||||
* @returns {GetRegistryItemShape<T>[]}
|
||||
*/
|
||||
getAll() {
|
||||
if (!this.elements) {
|
||||
const content = Object.values(this.content).sort((el1, el2) => el1[0] - el2[0]);
|
||||
this.elements = content.map((elem) => elem[1]);
|
||||
}
|
||||
return this.elements.slice();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of all entries, ordered by sequence numbers.
|
||||
*
|
||||
* @returns {[string, GetRegistryItemShape<T>][]}
|
||||
*/
|
||||
getEntries() {
|
||||
if (!this.entries) {
|
||||
const entries = Object.entries(this.content).sort((el1, el2) => el1[1][0] - el2[1][0]);
|
||||
this.entries = entries.map(([str, elem]) => [str, elem[1]]);
|
||||
}
|
||||
return this.entries.slice();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an item from the registry
|
||||
*
|
||||
* @param {string} key
|
||||
*/
|
||||
remove(key) {
|
||||
const value = this.content[key];
|
||||
delete this.content[key];
|
||||
const payload = { operation: "delete", key, value };
|
||||
this.trigger("UPDATE", payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a sub registry (and create it if necessary)
|
||||
*
|
||||
* @template {keyof GetRegistryCategories<T> & string} K
|
||||
* @param {K} subcategory
|
||||
* @returns {Registry<GetRegistryCategories<T>[K]>}
|
||||
*/
|
||||
category(subcategory) {
|
||||
if (!(subcategory in this.subRegistries)) {
|
||||
this.subRegistries[subcategory] = new Registry(subcategory);
|
||||
}
|
||||
return this.subRegistries[subcategory];
|
||||
}
|
||||
|
||||
addValidation(schema) {
|
||||
if (this.validationSchema) {
|
||||
throw new Error("Validation schema already set on this registry");
|
||||
}
|
||||
this.validationSchema = schema;
|
||||
for (const [key, value] of this.getEntries()) {
|
||||
validateSchema(this.name, key, value, schema);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {Registry<import("registries").GlobalRegistry>} */
|
||||
export const registry = new Registry();
|
28
runbot/static/src/vendored/registry_hook.js
Normal file
28
runbot/static/src/vendored/registry_hook.js
Normal file
@ -0,0 +1,28 @@
|
||||
/** @odoo-module alias=@web/core/registry_hook default=false **/
|
||||
|
||||
|
||||
import { useState, onWillStart, onWillDestroy } from "@runbot/owl";
|
||||
|
||||
export function useRegistry(registry) {
|
||||
const state = useState({ entries: registry.getEntries() });
|
||||
const listener = ({ detail }) => {
|
||||
const index = state.entries.findIndex(([k]) => k === detail.key);
|
||||
if (detail.operation === "add" && index === -1) {
|
||||
// push the new entry at the right place
|
||||
const newEntries = registry.getEntries();
|
||||
const newEntry = newEntries.find(([k]) => k === detail.key);
|
||||
const newIndex = newEntries.indexOf(newEntry);
|
||||
if (newIndex === newEntries.length - 1) {
|
||||
state.entries.push(newEntry);
|
||||
} else {
|
||||
state.entries.splice(newIndex, 0, newEntry);
|
||||
}
|
||||
} else if (detail.operation === "delete" && index >= 0) {
|
||||
state.entries.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
onWillStart(() => registry.addEventListener("UPDATE", listener));
|
||||
onWillDestroy(() => registry.removeEventListener("UPDATE", listener));
|
||||
return state;
|
||||
}
|
320
runbot/static/src/vendored/template_inheritance.js
Normal file
320
runbot/static/src/vendored/template_inheritance.js
Normal file
@ -0,0 +1,320 @@
|
||||
/** @odoo-module alias=@web/core/template_inheritance default=false **/
|
||||
|
||||
const RSTRIP_REGEXP = /(?=\n[ \t]*$)/;
|
||||
/**
|
||||
* The child nodes of operation represent new content to create before target or
|
||||
* or other elements to move before target from the target tree (tree from which target is part of).
|
||||
* Some processing of text nodes has to be done in order to normalize the situation.
|
||||
* Note: we assume that target has a parent element.
|
||||
* @param {Element} target
|
||||
* @param {Element} operation
|
||||
*/
|
||||
function addBefore(target, operation) {
|
||||
const nodes = getNodes(target, operation);
|
||||
if (nodes.length === 0) {
|
||||
return;
|
||||
}
|
||||
const { previousSibling } = target;
|
||||
target.before(...nodes);
|
||||
if (previousSibling?.nodeType === Node.TEXT_NODE) {
|
||||
const [text1, text2] = previousSibling.data.split(RSTRIP_REGEXP);
|
||||
previousSibling.data = text1.trimEnd();
|
||||
if (nodes[0].nodeType === Node.TEXT_NODE) {
|
||||
mergeTextNodes(previousSibling, nodes[0]);
|
||||
}
|
||||
if (text2 && nodes.some((n) => n.nodeType !== Node.TEXT_NODE)) {
|
||||
const textNode = document.createTextNode(text2);
|
||||
target.before(textNode);
|
||||
if (textNode.previousSibling.nodeType === Node.TEXT_NODE) {
|
||||
mergeTextNodes(textNode.previousSibling, textNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* element is part of a tree. Here we return the root element of that tree.
|
||||
* Note: this root element is not necessarily the documentElement of the ownerDocument
|
||||
* of element (hence the following code).
|
||||
* @param {Element} element
|
||||
* @returns {Element}
|
||||
*/
|
||||
function getRoot(element) {
|
||||
while (element.parentElement) {
|
||||
element = element.parentElement;
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
const HASCLASS_REGEXP = /hasclass\(([^)]*)\)/g;
|
||||
const CLASS_CONTAINS_REGEX = /contains\(@class.*\)/g;
|
||||
/**
|
||||
* @param {Element} operation
|
||||
* @returns {string}
|
||||
*/
|
||||
function getXpath(operation) {
|
||||
const xpath = operation.getAttribute("expr");
|
||||
if (odoo.debug) {
|
||||
if (CLASS_CONTAINS_REGEX.test(xpath)) {
|
||||
const parent = operation.closest("t[t-inherit]");
|
||||
const templateName = parent.getAttribute("t-name") || parent.getAttribute("t-inherit");
|
||||
console.warn(
|
||||
`Error-prone use of @class in template "${templateName}" (or one of its inheritors).` +
|
||||
" Use the hasclass(*classes) function to filter elements by their classes"
|
||||
);
|
||||
}
|
||||
}
|
||||
// hasclass does not exist in XPath 1.0 but is a custom function defined server side (see _hasclass) usable in lxml.
|
||||
// Here we have to replace it by a complex condition (which is not nice).
|
||||
// Note: we assume that classes do not contain the 2 chars , and )
|
||||
return xpath.replaceAll(HASCLASS_REGEXP, (_, capturedGroup) => {
|
||||
return capturedGroup
|
||||
.split(",")
|
||||
.map((c) => `contains(concat(' ', @class, ' '), ' ${c.trim().slice(1, -1)} ')`)
|
||||
.join(" and ");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} element
|
||||
* @param {Element} operation
|
||||
* @returns {Node|null}
|
||||
*/
|
||||
function getNode(element, operation) {
|
||||
const root = getRoot(element);
|
||||
const doc = new Document();
|
||||
doc.appendChild(root); // => root is the documentElement of its ownerDocument (we do that in case root is a clone)
|
||||
if (operation.tagName === "xpath") {
|
||||
const xpath = getXpath(operation);
|
||||
const result = doc.evaluate(xpath, root, null, XPathResult.FIRST_ORDERED_NODE_TYPE);
|
||||
return result.singleNodeValue;
|
||||
}
|
||||
for (const elem of root.querySelectorAll(operation.tagName)) {
|
||||
if (
|
||||
[...operation.attributes].every(
|
||||
({ name, value }) => name === "position" || elem.getAttribute(name) === value
|
||||
)
|
||||
) {
|
||||
return elem;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} element
|
||||
* @param {Element} operation
|
||||
* @returns {Element}
|
||||
*/
|
||||
function getElement(element, operation) {
|
||||
const node = getNode(element, operation);
|
||||
if (!node) {
|
||||
throw new Error(`Element '${operation.outerHTML}' cannot be located in element tree`);
|
||||
}
|
||||
if (!(node instanceof Element)) {
|
||||
throw new Error(`Found node ${node} instead of an element`);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} element
|
||||
* @param {Element} operation
|
||||
* @returns {Node[]}
|
||||
*/
|
||||
function getNodes(element, operation) {
|
||||
const nodes = [];
|
||||
for (const childNode of operation.childNodes) {
|
||||
if (childNode.tagName === "xpath" && childNode.getAttribute?.("position") === "move") {
|
||||
const node = getElement(element, childNode);
|
||||
removeNode(node);
|
||||
nodes.push(node);
|
||||
} else {
|
||||
nodes.push(childNode);
|
||||
}
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Text} first
|
||||
* @param {Text} second
|
||||
* @param {boolean} [trimEnd=true]
|
||||
*/
|
||||
function mergeTextNodes(first, second, trimEnd = true) {
|
||||
first.data = (trimEnd ? first.data.trimEnd() : first.data) + second.data;
|
||||
second.remove();
|
||||
}
|
||||
|
||||
function splitAndTrim(str, separator) {
|
||||
return str.split(separator).map((s) => s.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} target
|
||||
* @param {Element} operation
|
||||
*/
|
||||
function modifyAttributes(target, operation) {
|
||||
for (const child of operation.children) {
|
||||
if (child.tagName !== "attribute") {
|
||||
continue;
|
||||
}
|
||||
const attributeName = child.getAttribute("name");
|
||||
const firstNode = child.childNodes[0];
|
||||
let value = firstNode?.nodeType === Node.TEXT_NODE ? firstNode.data : "";
|
||||
|
||||
const add = child.getAttribute("add") || "";
|
||||
const remove = child.getAttribute("remove") || "";
|
||||
if (add || remove) {
|
||||
if (firstNode?.nodeType === Node.TEXT_NODE) {
|
||||
throw new Error(`Useless element content ${firstNode.outerHTML}`);
|
||||
}
|
||||
const separator = child.getAttribute("separator") || ",";
|
||||
const toRemove = new Set(splitAndTrim(remove, separator));
|
||||
const values = splitAndTrim(target.getAttribute(attributeName) || "", separator).filter(
|
||||
(s) => !toRemove.has(s)
|
||||
);
|
||||
values.push(...splitAndTrim(add, separator).filter((s) => s));
|
||||
value = values.join(separator);
|
||||
}
|
||||
|
||||
if (value) {
|
||||
target.setAttribute(attributeName, value);
|
||||
} else {
|
||||
target.removeAttribute(attributeName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove node and normalize surrounind text nodes (if any)
|
||||
* Note: we assume that node has a parent element
|
||||
* @param {Node} node
|
||||
*/
|
||||
function removeNode(node) {
|
||||
const { nextSibling, previousSibling } = node;
|
||||
node.remove();
|
||||
if (nextSibling?.nodeType === Node.TEXT_NODE && previousSibling?.nodeType === Node.TEXT_NODE) {
|
||||
mergeTextNodes(
|
||||
previousSibling,
|
||||
nextSibling,
|
||||
previousSibling.parentElement.firstChild === previousSibling
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} root
|
||||
* @param {Element} target
|
||||
* @param {Element} operation
|
||||
*/
|
||||
function replace(root, target, operation) {
|
||||
const mode = operation.getAttribute("mode") || "outer";
|
||||
switch (mode) {
|
||||
case "outer": {
|
||||
const result = operation.ownerDocument.evaluate(
|
||||
".//*[text()='$0']",
|
||||
operation,
|
||||
null,
|
||||
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE
|
||||
);
|
||||
for (let i = 0; i < result.snapshotLength; i++) {
|
||||
const loc = result.snapshotItem(i);
|
||||
loc.firstChild.replaceWith(target.cloneNode(true));
|
||||
}
|
||||
if (target.parentElement) {
|
||||
const nodes = getNodes(target, operation);
|
||||
target.replaceWith(...nodes);
|
||||
} else {
|
||||
let operationContent = null;
|
||||
let comment = null;
|
||||
for (const child of operation.childNodes) {
|
||||
if (child.nodeType === Node.ELEMENT_NODE) {
|
||||
operationContent = child;
|
||||
break;
|
||||
}
|
||||
if (child.nodeType === Node.COMMENT_NODE) {
|
||||
comment = child;
|
||||
}
|
||||
}
|
||||
root = operationContent.cloneNode(true);
|
||||
if (target.hasAttribute("t-name")) {
|
||||
root.setAttribute("t-name", target.getAttribute("t-name"));
|
||||
}
|
||||
if (comment) {
|
||||
root.prepend(comment);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "inner":
|
||||
while (target.firstChild) {
|
||||
target.removeChild(target.lastChild);
|
||||
}
|
||||
target.append(...operation.childNodes);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Invalid mode attribute: '${mode}'`);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} root
|
||||
* @param {Element} operations is a single element whose children represent operations to perform on root
|
||||
* @param {string} [url=""]
|
||||
* @returns {Element} root modified (in place) by the operations
|
||||
*/
|
||||
export function applyInheritance(root, operations, url = "") {
|
||||
for (const operation of operations.children) {
|
||||
const target = getElement(root, operation);
|
||||
const position = operation.getAttribute("position") || "inside";
|
||||
|
||||
if (odoo.debug && url) {
|
||||
const attributes = [...operation.attributes].map(
|
||||
({ name, value }) =>
|
||||
`${name}=${JSON.stringify(name === "position" ? position : value)}`
|
||||
);
|
||||
const comment = document.createComment(
|
||||
` From file: ${url} ; ${attributes.join(" ; ")} `
|
||||
);
|
||||
if (position === "attributes") {
|
||||
target.before(comment); // comment won't be visible if target is root
|
||||
} else {
|
||||
operation.prepend(comment);
|
||||
}
|
||||
}
|
||||
|
||||
switch (position) {
|
||||
case "replace": {
|
||||
root = replace(root, target, operation); // root can be replaced (see outer mode)
|
||||
break;
|
||||
}
|
||||
case "attributes": {
|
||||
modifyAttributes(target, operation);
|
||||
break;
|
||||
}
|
||||
case "inside": {
|
||||
const sentinel = document.createElement("sentinel");
|
||||
target.append(sentinel);
|
||||
addBefore(sentinel, operation);
|
||||
removeNode(sentinel);
|
||||
break;
|
||||
}
|
||||
case "after": {
|
||||
const sentinel = document.createElement("sentinel");
|
||||
target.after(sentinel);
|
||||
addBefore(sentinel, operation);
|
||||
removeNode(sentinel);
|
||||
break;
|
||||
}
|
||||
case "before": {
|
||||
addBefore(target, operation);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Invalid position attribute: '${position}'`);
|
||||
}
|
||||
}
|
||||
return root;
|
||||
}
|
176
runbot/static/src/vendored/templates.js
Normal file
176
runbot/static/src/vendored/templates.js
Normal file
@ -0,0 +1,176 @@
|
||||
/** @odoo-module alias=@web/core/templates default=false */
|
||||
|
||||
import { applyInheritance } from "@web/core/template_inheritance";
|
||||
|
||||
const parser = new DOMParser();
|
||||
/** @type {((document: Document) => void)[]} */
|
||||
const templateProcessors = [];
|
||||
/** @type {((url: string) => boolean)[]} */
|
||||
let urlFilters = [];
|
||||
function getParsedTemplate(templateString) {
|
||||
const doc = parser.parseFromString(templateString, "text/xml");
|
||||
for (const processor of templateProcessors) {
|
||||
processor(doc);
|
||||
}
|
||||
return doc.firstChild;
|
||||
}
|
||||
|
||||
function getClone(template) {
|
||||
const c = template.cloneNode(true);
|
||||
new Document().append(c); // => c is the documentElement of its ownerDocument
|
||||
return c;
|
||||
}
|
||||
|
||||
const registered = new Set();
|
||||
function isRegistered(...args) {
|
||||
const key = JSON.stringify([...args]);
|
||||
if (registered.has(key)) {
|
||||
return true;
|
||||
}
|
||||
registered.add(key);
|
||||
return false;
|
||||
}
|
||||
|
||||
let blockType = null;
|
||||
let blockId = 0;
|
||||
|
||||
const templates = {};
|
||||
const parsedTemplates = {};
|
||||
const info = {};
|
||||
export function registerTemplate(name, url, templateString) {
|
||||
if (isRegistered(...arguments)) {
|
||||
return;
|
||||
}
|
||||
if (blockType !== "templates") {
|
||||
blockType = "templates";
|
||||
blockId++;
|
||||
}
|
||||
if (name in templates && (info[name].url !== url || templates[name] !== templateString)) {
|
||||
throw new Error(`Template ${name} already exists`);
|
||||
}
|
||||
templates[name] = templateString;
|
||||
info[name] = { blockId, url };
|
||||
}
|
||||
|
||||
const templateExtensions = {};
|
||||
const parsedTemplateExtensions = {};
|
||||
export function registerTemplateExtension(inheritFrom, url, templateString) {
|
||||
if (isRegistered(...arguments)) {
|
||||
return;
|
||||
}
|
||||
if (blockType !== "extensions") {
|
||||
blockType = "extensions";
|
||||
blockId++;
|
||||
}
|
||||
if (!templateExtensions[inheritFrom]) {
|
||||
templateExtensions[inheritFrom] = [];
|
||||
}
|
||||
if (!templateExtensions[inheritFrom][blockId]) {
|
||||
templateExtensions[inheritFrom][blockId] = [];
|
||||
}
|
||||
templateExtensions[inheritFrom][blockId].push({
|
||||
templateString,
|
||||
url,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(document: Document) => void} processor
|
||||
*/
|
||||
export function registerTemplateProcessor(processor) {
|
||||
templateProcessors.push(processor);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {typeof urlFilters} filters
|
||||
*/
|
||||
export function setUrlFilters(filters) {
|
||||
urlFilters = filters;
|
||||
}
|
||||
|
||||
function _getTemplate(name, blockId = null) {
|
||||
if (!(name in parsedTemplates)) {
|
||||
if (!(name in templates)) {
|
||||
return null;
|
||||
}
|
||||
const templateString = templates[name];
|
||||
parsedTemplates[name] = getParsedTemplate(templateString);
|
||||
}
|
||||
let processedTemplate = parsedTemplates[name];
|
||||
|
||||
const inheritFrom = processedTemplate.getAttribute("t-inherit");
|
||||
if (inheritFrom) {
|
||||
const parentTemplate = _getTemplate(inheritFrom, blockId || info[name].blockId);
|
||||
if (!parentTemplate) {
|
||||
throw new Error(
|
||||
`Constructing template ${name}: template parent ${inheritFrom} not found`
|
||||
);
|
||||
}
|
||||
const element = getClone(processedTemplate);
|
||||
processedTemplate = applyInheritance(getClone(parentTemplate), element, info[name].url);
|
||||
if (processedTemplate.tagName !== element.tagName) {
|
||||
const temp = processedTemplate;
|
||||
processedTemplate = new Document().createElement(element.tagName);
|
||||
processedTemplate.append(...temp.childNodes);
|
||||
}
|
||||
for (const { name, value } of element.attributes) {
|
||||
if (!["t-inherit", "t-inherit-mode"].includes(name)) {
|
||||
processedTemplate.setAttribute(name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const otherBlockId in templateExtensions[name] || {}) {
|
||||
if (blockId && otherBlockId > blockId) {
|
||||
break;
|
||||
}
|
||||
if (!(name in parsedTemplateExtensions)) {
|
||||
parsedTemplateExtensions[name] = {};
|
||||
}
|
||||
if (!(otherBlockId in parsedTemplateExtensions[name])) {
|
||||
parsedTemplateExtensions[name][otherBlockId] = [];
|
||||
for (const { templateString, url } of templateExtensions[name][otherBlockId]) {
|
||||
parsedTemplateExtensions[name][otherBlockId].push({
|
||||
template: getParsedTemplate(templateString),
|
||||
url,
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const { template, url } of parsedTemplateExtensions[name][otherBlockId]) {
|
||||
if (!urlFilters.every((filter) => filter(url))) {
|
||||
continue;
|
||||
}
|
||||
processedTemplate = applyInheritance(
|
||||
inheritFrom ? processedTemplate : getClone(processedTemplate),
|
||||
getClone(template),
|
||||
url
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return processedTemplate;
|
||||
}
|
||||
|
||||
/** @type {Record<string, Element>} */
|
||||
let processedTemplates = {};
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
*/
|
||||
export function getTemplate(name) {
|
||||
if (!processedTemplates[name]) {
|
||||
processedTemplates[name] = _getTemplate(name);
|
||||
}
|
||||
return processedTemplates[name];
|
||||
}
|
||||
|
||||
export function clearProcessedTemplates() {
|
||||
processedTemplates = {};
|
||||
}
|
||||
|
||||
export function checkPrimaryTemplateParents(namesToCheck) {
|
||||
const missing = new Set(namesToCheck.filter((name) => !(name in templates)));
|
||||
if (missing.size) {
|
||||
console.error(`Missing (primary) parent templates: ${[...missing].join(", ")}`);
|
||||
}
|
||||
}
|
@ -15,6 +15,8 @@
|
||||
<script type="text/javascript" src="/runbot/static/libs/bootstrap/js/bootstrap.bundle.js"/>
|
||||
<script type="text/javascript" src="/runbot/static/src/frontend/runbot.js"/>
|
||||
|
||||
<t t-call-assets="runbot.assets_frontend"/>
|
||||
|
||||
<t t-if="refresh">
|
||||
<meta http-equiv="refresh" t-att-content="refresh"/>
|
||||
</t>
|
||||
|
Loading…
Reference in New Issue
Block a user