@seanox/aspect-js
Version:
full stack JavaScript framework for SPAs incl. reactivity rendering, mvc / mvvm, models, expression language, datasource, virtual paths, unit test and some more
985 lines (890 loc) • 43.1 kB
JavaScript
/**
* LIZENZBEDINGUNGEN - Seanox Software Solutions ist ein Open-Source-Projekt,
* im Folgenden Seanox Software Solutions oder kurz Seanox genannt.
* Diese Software unterliegt der Version 2 der Apache License.
*
* Seanox aspect-js, fullstack for single page applications
* Copyright (C) 2023 Seanox Software Solutions
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*
*
* DESCRIPTION
* ----
* Static component for the use of a SiteMap for virtual paths. SiteMap is a
* directory consisting of faces and facets that are addressed by paths.
*
* +-----------------------------------------------+
* | Page |
* | +-----------------------------------------+ |
* | | Face A / Partial Face A | |
* | | +-------------+ +-------------+ | |
* | | | Facet A1 | ... | Facet An | | |
* | | +-------------+ +-------------+ | |
* | | | |
* | | +-----------------------------------+ | |
* | | | Face AA | | |
* | | | +-----------+ +-----------+ | | |
* | | | | Facet AA1 | ... | Facet AAn | | | |
* | | | +-----------+ +-----------+ | | |
* | | +-----------------------------------+ | |
* | | ... | |
* | +-----------------------------------------+ |
* | ... |
* | +-----------------------------------------+ |
* | | Face n | |
* | | ... | |
* | +-----------------------------------------+ |
* +-----------------------------------------------+
*
* A face is the primary projection of the content. This projection may contain
* additional sub-components, in form of facets and sub-faces.
*
* Facets are parts of a face (projection) and are not normally a standalone
* component. For example, the input mask and result table of a search can be
* separate facets of a face, as can articles or sections of a face. Both face
* and facet can be accessed via virtual paths. The path to a facet has the
* effect that the face is displayed with any other faces. The facet is no
* longer automatically focused, because the own implementation is very simple
* and much flexible.
*
* window.addEventListener("hashchange", (event) => {
* var path = Path.normalize(event.newURL || "#");
* var target = SiteMap.lookup(path);
* if (target) {
* target = target.facet || target.face;
* if (target) {
* target = target.replace(/(?!=#)#/, " #");
* target = document.querySelector(target);
* if (target) {
* target.scrollIntoView(true);
* target.focus();
* }
* }
* }
* });
*
* Faces are also components that can be nested. Thus, parent faces become
* partial faces when the path refers to a sub-face. A sub-face is presented
* with all its parent partial faces. If the parent faces contain additional
* facets, these facets are not displayed. The parent faces are therefore only
* partially presented.
*
* With the SiteMap the structure of faces, facets and the corresponding paths
* are described. The SiteMap controls the face flow and the presentation of
* the components corresponding to a path. This means that you don't have to
* take care of showing and hiding components yourself.
*
* The show and hide is hard realized in the DOM. This means that if a component
* is hidden, it is physically removed from the DOM and only added again when it
* is displayed.
*
* When it comes to controlling face flow, the SiteMap provides hooks for
* permission concepts and acceptors. With both things the face flow can be
* controlled and influenced. This way, the access to paths can be stopped
* and/or redirected/forwarded with own logic.
*
* The view model binding part also belongs to the Model View Controller and is
* taken over by the Composite API in this implementation. SiteMap is an
* extension and is based on the Composite API.
*
* @author Seanox Software Solutions
* @version 1.6.0 20230328
*/
(() => {
/**
* Pattern for a valid face path:
* - Paths and path segments must always begin with a word character
* - Allowed are the word characters a-z _ 0-9 and additionally -
* - Character - always embedded between the characters: a-z _ 0-9, it
* can not be used at the beginning and end
* - Character # is used to separate the path segments
* - After the separator # at least a word character is expected
* - Only # as root path is also allowed
*/
const PATTERN_PATH_FACE = /(^((#\w[\-\w]+\w)|(#\w+))+$)|(^#$)/;
/**
* Pattern for a valid facet path:
* - Paths and path segments must always begin with a word character
* - Allowed are the word characters a-z _ 0-9 and additionally -
* - Character - always embedded between the characters: a-z _ 0-9, it
* can not be used at the beginning and end
* - Character # is used to separate the path segments
* - After the separator # at least a word character is expected
*/
const PATTERN_PATH_FACET = /^((\w[\-\w]+\w)|(\w+))(#((\w[\-\w]+\w)|(\w+)))*$/;
/**
* Pattern for a valid variable facet path:
* - Same conditions as for the pattern for a valid facet path
* - Every path must end with ...
* - Only ... as facet path is also allowed
*/
const PATTERN_PATH_FACET_VARIABLE = /(^((\w[\-\w]+\w)|(\w+))(#((\w[\-\w]+\w)|(\w+)))*(\.){3}$)|(^\.{3}$)/;
let _sitemap_active = false;
compliant("SiteMap");
compliant(null, window.SiteMap = {
/**
* Primarily, the root is always used when loading the page, since the
* renderer is executed before the MVC. Therefore, the renderer may not
* yet know all the paths and authorizations. After the renderer, the
* MVC is loaded and triggers the renderer again if the path requested
* with the URL differs.
*/
get location() {
if (_history.size <= 0)
return window.location.hash || "#";
const history = Array.from(_history);
return history[history.length -1];
},
set location(path) {
if (_history.has(path)) {
const values = Array.from(_history.values());
while (_history.has(path)
&& values.includes(path)) {
const entry = values.pop();
if (entry === path)
return;
_history.delete(entry);
}
} else _history.add(path);
},
/**
* Checks a path against existing/registered permission methods. A path
* is allowed only if all permission methods return true.
* @param path to check (URL is also supported, only the hash is used
* here and the URL itself is ignored)
* @return true if the path has been confirmed as permitted
*/
permit(path) {
for (let acceptor of _acceptors) {
if (acceptor.pattern
&& !acceptor.pattern.test(path))
continue;
acceptor = acceptor.action.call(null, path);
if (acceptor !== true) {
if (typeof acceptor === "string")
acceptor = Path.normalize(acceptor);
return acceptor;
}
}
return true;
},
/**
* Determines the real existing path according to the SiteMap. The
* methods distinguish between absolute, relative and functional paths.
* The functionalities remain unchanged. Absolute and relative paths are
* balanced. Relative paths are balanced on the basis of the current
* location. All paths are checked against the SiteMap. Invalid paths
* are searched for a valid partial path. To do this, the path is
* shortened piece by piece. If no valid partial path can be found, the
* root is returned. Without passing a path, the current location is
* returned.
* @param path to check - optional (URL is also supported, only the
* hash is used here and the URL itself is ignored)
* @return the real path determined in the SiteMap, or the unchanged
* function path.
*/
locate(path) {
path = path || "";
// The path is normalized.
// Invalid paths are shortened when searching for a valid partial
// path. Theoretically, the shortening must end automatically with
// the root or the current path.
const locate = (path) => {
const variants = [SiteMap.location, path];
if (path.match(/(^#[^#].*$)|(^#$)/))
variants.shift();
return variants;
};
try {path = Path.normalize(...locate(path));
} catch (error) {
while (true) {
path = path.replace(/(^[^#]+$)|(#[^#]*$)/, "");
try {path = Path.normalize(...locate(path));
} catch (error) {
continue;
}
break;
}
}
if (path.match(PATTERN_PATH_FUNCTIONAL))
return path;
let paths = Array.from(_paths.keys());
paths = paths.concat(Array.from(_facets.keys()));
while (paths && path.length > 1) {
for (const variable of _variables)
if ((path + "#").startsWith(variable + "#"))
return path;
if (paths.includes(path))
return path;
path = Path.normalize(path + "##");
}
return "#";
},
/**
* Navigates to the given path, if it exists in the SiteMap. All paths
* are checked against the SiteMap. Invalid paths are searched for a
* valid partial path. To do this, the path is shortened piece by piece.
* If no valid partial path can be found, the root is the target.
*
* In difference to the forward method, navigate is not executed
* directly, instead the change is triggered asynchronous by the
* location hash.
*
* @param path (URL is also supported, only the hash is used here and
* the URL itself is ignored)
*/
navigate(path) {
Composite.asynchron((path) => {
window.location.hash = path;
}, SiteMap.locate(path));
SiteMap.locate(path)
},
/**
* Returns the meta data for a path.
* The meta data is an object with the following structure:
*
* {path:..., face:..., facet:...}
*
* If variable paths are used, an additional data field is available. It
* contains the additional data passed with the path, comparable to
* PATH_INFO in CGI.
*
* {path:..., face:..., facet:..., data:...}
*
* If no meta data can be determined because the path is invalid or not
* declared in the SiteMap, null is returned.
* @param path optional, without SiteMap.location is used
* @return meta data object, otherwise null
*/
lookup(path) {
if (arguments.length <= 0)
path = SiteMap.location;
const canonical = (meta) => {
if (!meta.facet)
return meta.path;
if (meta.path.endsWith("#"))
return meta.path + meta.facet;
return meta.path + "#" + meta.facet;
};
for (const variable of _variables) {
if (!(path + "#").startsWith(variable + "#"))
continue;
if (_paths.has(variable)) {
return {path, face:variable, facet:null, get data() {
return path.substring(variable.length) || null;
}};
} else if (_facets.has(variable)) {
const facet = _facets.get(variable);
return {path, face:facet.path, facet:facet.facet, get data() {
return path.substring(facet.path.length +facet.facet.length) || null;
}};
}
break;
}
if (_paths.has(path)) {
return {path, face:path, facet:null};
} else if (_facets.has(path)) {
const facet = _facets.get(path);
return {path:canonical(facet), face:facet.path, facet:facet.facet};
}
return null;
},
/**
* Forwards to the given path, if it exists in the SiteMap. All paths
* are checked against the SiteMap. Invalid paths are searched for a
* valid partial path. To do this, the path is shortened piece by piece.
* If no valid partial path can be found, the root is the target.
*
* In difference to the navigate method, the forwarding is executed
* directly, instead the navigate method triggers asynchronous
* forwarding by changing the location hash.
*
* @param path (URL is also supported, only the hash is used here and
* the URL itself is ignored)
*/
forward(path) {
const event = new Event("hashchange",{bubbles:false, cancelable:true})
event.newURL = path;
window.dispatchEvent(event);
},
/**
* Checks whether a path or sub-path is currently being used. This is
* used to show/hide composites depending on the current location.
* @param path
* @return true if the (sub)path is currently used, otherwise false
*/
accept(path) {
// Only valid paths can be confirmed.
path = (path || "").trim().toLowerCase();
if (!path.match(/^#.*$/))
return false;
path = path.replace(/(#.*?)#*$/, "$1");
// The current path is determined and it is determined whether it is
// a face or a facet. In both cases, a meta object is created:
// {path:#path, facet:...}
const location = SiteMap.lookup(Path.normalize(SiteMap.location));
if (!location)
return false;
// Determines whether the passed path is a face, partial face or
// facet. (Partial)faces always have the higher priority for facets.
// If nothing can be determined, there cannot be a valid path.
const lookup = SiteMap.lookup(path);
if (!lookup)
return false;
let partial = lookup.path;
if (!partial.endsWith("#"))
partial += "#";
// Facets are only displayed if the paths match and the path does
// not refer to a partial face.
if (lookup.facet
&& location.face !== lookup.face
&& !location.path.startsWith(partial))
return false;
// Faces and partial faces are only displayed if the paths match or
// the path starts with the passed path as a partial path.
if (!location.path.startsWith(partial)
&& location.face !== lookup.face)
return false;
// Invalid paths and facets are excluded at this place, because they
// already cause a false termination of this method (return false)
// and do not have to be checked here anymore.
return true;
},
/**
* Configures the SiteMap individually. The configuration is passed as a
* meta object. The keys (string) correspond to the paths, the values
* are arrays with the valid facets for a path.
*
* sitemap = {
* "#": ["news", "products", "about", "contact", "legal"],
* "#products#papers": ["paperA4", "paperA5", "paperA6"],
* "#products#envelope": ["envelopeA4", "envelopeA5", "envelopeA6"],
* "#products#pens": ["pencil", "ballpoint", "stylograph"],
* "#legal": ["terms", "privacy"],
* ...
* };
*
* sitemap = {
* "#": ["news", "products", "about", "contact...", "legal"],
* "#product": ["..."],
* ...
* };
*
* The method has different signatures and can be called several times.
* If the method is called more than once, the configurations are
* concatenated and existing values in the configuration are overwritten.
*
* The following signatures are available:
*
* SiteMap.customize({meta});
* SiteMap.customize({meta}, function(path) {...});
* SiteMap.customize(RegExp, function(path) {...});
*
* SiteMap as meta object:
* ----
* The first configuration describes the SiteMap as a meta object. The
* meta object defines all available paths and facets for the face flow.
*
* SiteMap as meta object and permit function:
* ----
* In the second example, a permit method is passed in addition to the
* SiteMap as a meta object. This method is used to implement permission
* concepts and can be used to check and manipulate paths. Several
* permit methods can be registered. All requested paths pass through
* the permit method(s). This can decide what happens to the path. From
* the permit method a return value is expected, which can have the
* following characteristics:
*
* true:
* The validation is successful and the iteration via further permit
* method is continued. If all permit methods return true and thus
* confirm the path, it is used.
*
* String:
* The validation (iteration over further permit-methods) will be
* aborted and it will be forwarded to the path corresponding to the
* string.
*
* In all other cases:
* The path is regarded as invalid/unauthorized, the validation
* (iteration over further permit-methods) will be aborted and is
* forwarded to the original path.
*
* A permit method for paths can optionally be passed to each meta
* object. This is interesting for modules that want to register and
* validate their own paths.
*
* Acceptor:
* ----
* Acceptors work in a similar way to permit methods. In difference,
* permit methods are called for each path and acceptors are only called
* for those that match the RegExp pattern. Also from the permit method
* a return value is expected, which can have the following
* characteristics -- see SiteMap with permit function.
*
* SiteMap.customize(/^phone.*$/i, function(path) {
* dial the phone number
* });
* SiteMap.customize(/^mail.*$/i, function(path) {
* send a mail
* });
* SiteMap.customize(/^sms.*$/i, function(path) {
* send a sms
* });
*
* SiteMap.customize(RegExp, function);
*
* Permit methods and acceptors are regarded as one set and called in
* the order of their registration.
*
* Important note about how the SiteMap works:
* ----
* The SiteMap collects all configurations cumulatively. All paths and
* facets are summarized, acceptors and permit methods are collected in
* the order of their registration. A later determination of which
* metadata was registered with which permit methods is not possible.
*
* The configuration of the SiteMap is only applied if an error-free
* meta object is passed and no errors occur during processing.
*
* The method uses variable parameters as and according to the previous
* description.
*
* @param pattern
* @param callback
* @param meta
* @param permit
* @throws An error occurs in the following cases:
* - if the data type of acceptor and/or callback is invalid
* - if the data type of map and/or permit is invalid
* - if the syntax and/or the format of facets are invalid
*/
customize(...variants) {
_sitemap_active = true;
if (variants.length > 1
&& variants[0] instanceof RegExp) {
if (typeof variants[1] !== "function")
throw new TypeError("Invalid acceptor: " + typeof variants[1]);
_acceptors.add({pattern:variants[0], action:variants[1]});
return;
}
if (variants.length < 1
|| typeof variants[0] !== "object")
throw new TypeError("Invalid map: " + typeof variants[0]);
const map = variants[0];
const acceptors = new Set(_acceptors);
if (variants.length > 1) {
if (typeof variants[1] !== "function")
throw new TypeError("Invalid permit: " + typeof variants[1]);
acceptors.add({pattern:null, action:variants[1]});
}
const paths = new Map();
_paths.forEach((value, key) => {
if (typeof key === "string"
&& key.match(PATTERN_PATH_FACE))
paths.set(key, value);
});
const facets = new Map();
_facets.forEach((value, key) => {
if (typeof key === "string"
&& key.match(PATTERN_PATH_FACET))
facets.set(key, value);
});
let variables = new Set();
_variables.forEach((variable) => {
if (typeof variable === "string"
&& variable.match(PATTERN_PATH_FACE))
variables.add(variable);
});
Object.keys(map).forEach((key) => {
// A map entry is based on a path (datatype string beginning
// with #) and an array of String or null as value.
if (typeof key !== "string"
|| !key.match(PATTERN_PATH_FACE))
return;
let value = map[key];
if (value !== null
&& !Array.isArray(value))
return;
key = Path.normalize(key);
// Entry is added to the path map, if necessary as empty array.
// Thus, the following path map object will be created:
// {#path:[facet, facet, ...], ...}
if (!paths.has(key))
paths.set(key, []);
// In the next step, the facets for a path are determined.
// These are added to the path in the path map if these do not
// already exist there.
// Additionally, a facet map object will be created:
// {#facet-path:{path:#path, facet:facet}, ...}
value = value || [];
value.forEach((facet) => {
// Facets is an array of strings with the names of the
// facets that must match the PATTERN_PATH_FACET.
if (typeof facet !== "string")
throw new TypeError("Invalid facet: " + typeof facet);
facet = facet.toLowerCase().trim();
if (!facet.match(PATTERN_PATH_FACET)
&& !facet.match(PATTERN_PATH_FACET_VARIABLE))
throw new Error(`Invalid facet${facet ? ": " + facet : ""}`);
// Variable paths are collected additionally, so that later
// on when determining the path, the complete SiteMap does
// not have to be searched for variable paths. Variable
// paths are also registered without ... at the end as
// normal paths.
if (facet.match(PATTERN_PATH_FACET_VARIABLE)) {
// If the facet is only ..., it is registered as
// available face, otherwise as a variable facet.
facet = facet.replace(/\.+$/, "");
const variable = facet ? key.replace(/#+$/, "") + "#" + facet : key;
if (!variables.has(variable))
variables.add(variable);
// If the face is only ..., it is registered as a face.
// Therefore, nothing needs to be done now.
if (!facet)
return;
}
// If the facet is not in the path, it will be added.
if (!paths.get(key).includes(facet))
paths.get(key).push(facet);
// The facet map object is assembled.
facets.set(Path.normalize(key, facet), {path:key, facet});
});
});
_acceptors.clear();
acceptors.forEach(value =>
_acceptors.add(value));
_paths.clear();
paths.forEach((value, key) =>
_paths.set(key, value));
_facets.clear();
facets.forEach((value, key) =>
_facets.set(key, value));
_variables.clear();
variables = Array.from(variables);
variables.sort((value1, value2) =>
value1.localeCompare(value2));
variables.forEach((value) =>
_variables.add(value));
}
});
// Map with all paths without facets paths (key:path, value:facets)
const _paths = new Map();
// Map with all paths with facet paths {path:key, facet:facet}
// The sum of all paths and assigned facets
const _facets = new Map();
// Set with all variables paths
const _variables = new Set();
// Set with all supported acceptors
const _acceptors = new Set();
// Set with the path history (optimized)
const _history = new Set();
Object.defineProperty(SiteMap, "history", {
value: _history
});
/**
* Registration of the attribute static for hardening. The attribute is
* therefore more difficult to manipulate in markup.
*/
Composite.customize("@ATTRIBUTES-STATICS", "static");
/**
* Established a listener that listens when the page loads. The method
* initiates the initial usage of the path.
*/
window.addEventListener("load", () => {
// When clicking on a link with the current path, the focus must be set
// back to face/facet, as the user may have scrolled on the page.
// However, this is only necessary if face + facet have not changed. In
// all other cases the Window-HashChange-Event does the same
document.body.addEventListener("click", (event) => {
if (!_sitemap_active)
return;
if (event.target
&& event.target instanceof Element
&& event.target.hasAttribute("href")) {
const target = SiteMap.lookup(event.target.getAttribute("href"));
const source = SiteMap.lookup(Path.normalize(SiteMap.location));
if (source && target
&& source.face === target.face
&& source.facet === target.facet
&& typeof target.focus === "function")
target.focus();
}
});
// The initial rendering is started by the direct call of the hashchange
// event, thus without trigger.
SiteMap.forward(window.location.hash || "#");
});
/**
* Establishes a listener that detects changes to the URL hash. The method
* corrects invalid and unauthorized paths by forwarding them to next valid
* path, restores the facet of functional paths, and organizes partial
* rendering.
*/
window.addEventListener("hashchange", (event) => {
// Without a SiteMap no partially rendering can be initiated.
if (!_sitemap_active
|| _paths.size <= 0)
return;
let source = Path.normalize(SiteMap.location);
let locate = (event.newURL || "").replace(PATTERN_URL, "$1");
let target = SiteMap.locate(locate);
// Indicator for initial rendering when the history is empty.
// In case of the initial rendering:
// - SiteMap.location must be set finally
// - window.location.hash must be set finally
// - Body must be rendered
const initial = _history.size <= 0;
// Initially, no function path is useful, and so in this case will be
// forwarded to the root.
if (target.match(PATTERN_PATH_FUNCTIONAL)
&& initial)
target = "#";
// For functional interaction paths, old paths are visually restored.
// Rendering is not necessary because the face/facet does not change or
// the called function has partially triggered rendering.
if (target.match(PATTERN_PATH_FUNCTIONAL)) {
history.replaceState(null, document.title, SiteMap.location);
return;
}
// If window.location.hash, SiteMap.location and new URL match, no
// update or rendering is required.
if (target === window.location.hash
&& target === SiteMap.location
&& !initial)
return;
// If the permission is not confirmed, will be forwarded to the next
// higher known/permitted path, based on the requested path.
// Alternatively, the permit methods can also supply a new target, which
// is then jumped to.
const forward = SiteMap.permit(target);
if (forward !== true) {
if (typeof forward === "string")
SiteMap.forward(forward);
else SiteMap.forward(target !== "#" ? target + "##" : "#");
return;
}
// The locations are updated synchronously. The possible triggering of
// the hashchange-event can be ignored, because SiteMap.location and
// window.location.hash are the same and therefore no update or
// rendering is triggered.
SiteMap.location = target;
// Source and target for rendering are determined. Because of possible
// variable paths, the current path does not have to correspond to the
// current face/facet and must be determined via the parent if necessary.
source = SiteMap.lookup(source);
target = SiteMap.lookup(target);
source = source.face;
if (!source.endsWith("#"))
source += "#";
target = target.face;
if (!target.endsWith("#"))
target += "#";
// The deepest similarity in both paths is used for rendering.
// Render as minimal as possible:
// old: #a#b#c#d#e#f new: #a#b#c#d -> render #d
// old: #a#b#c#d new: #a#b#c#d#e#f -> render #d
// old: #a#b#c#d new: #e#f -> render # (body)
// Initially always the body is rendered.
let render = "#";
if (source.startsWith(target))
render = target;
else if (target.startsWith(source))
render = source;
render = render.match(/((#[^#]+)|(^))#*$/);
if (render && render[1] && !initial)
Composite.render(render[1]);
else Composite.render(document.body);
// Focus only after rendering, so that the target is there.
window.location.assign(SiteMap.location);
// Update of the hash and thus of the page focus, if the new focus
// (hash) was hidden before rendering or did not exist. For this
// purpose, the setter must be called in window.location.
window.location.hash = window.location.hash;
});
/**
* Rendering filter for all composite elements. The filter causes that for
* each composite element determined by the renderer, an additional
* condition is added to the SiteMap. This condition is used to show or hide
* the composite elements in the DOM to the corresponding virtual paths. The
* elements are identified by the serial. The SiteMap uses a map
* (serial/element) as cache for fast access. The cleaning of the cache is
* done by a MutationObserver.
*/
Composite.customize((element) => {
if (!_sitemap_active)
return;
if (!(element instanceof Element)
|| !element.hasAttribute(Composite.ATTRIBUTE_COMPOSITE))
return;
if (element.hasAttribute("static"))
return;
let path = "";
for (let scope = element; scope; scope = scope.parentNode) {
if (!(scope instanceof Element)
|| !scope.hasAttribute(Composite.ATTRIBUTE_COMPOSITE))
continue;
const serial = (scope.getAttribute(Composite.ATTRIBUTE_ID) || "").trim();
if (!serial.match(Composite.PATTERN_COMPOSITE_ID))
throw new Error(`Invalid composite id${serial ? ": " + serial : ""}`);
path = "#" + serial + path;
}
let script = null;
if (element.hasAttribute(Composite.ATTRIBUTE_CONDITION)) {
script = element.getAttribute(Composite.ATTRIBUTE_CONDITION).trim();
if (script.match(Composite.PATTERN_EXPRESSION_CONTAINS))
script = script.replace(Composite.PATTERN_EXPRESSION_CONTAINS, (match) => {
match = match.substring(2, match.length -2).trim();
return `{{SiteMap.accept("${path}") and (${match})}}`;
});
}
if (!script)
script = `${script || ""}{{SiteMap.accept("${path}")}}`;
element.setAttribute(Composite.ATTRIBUTE_CONDITION, script);
});
/**
* Pattern for a valid path. The syntax is based on XML entities (DOM). A
* path segment begins with a word character _ a-z 0-9, optionally more word
* characters and additionally - can follow, but can not end with the -
* character. Paths are separated by the # character.
*/
const PATTERN_PATH = /(^(\w(\-*\w)*)*((#+\w(\-*\w)*)+)#*$)|(^\w(\-*\w)*$)|(^#+$)|(^$)/;
/** Pattern for an url path. */
const PATTERN_URL = /^\w+:\/.*?(#.*)*$/i;
/** Pattern for a functional path. */
const PATTERN_PATH_FUNCTIONAL = /^#{3,}$/;
/**
* Static component for the use of (virtual) paths. Paths are a reference to
* a target in face flow. The target can be a face, a facet or a function.
* For more details see method Path.normalize(variants).
*/
compliant("Path");
compliant(null, window.Path = {
/**
* Normalizes a path. Paths consist exclusively of word characters and
* underscores (based on composite IDs) and must begin with a word
* character and use the hash character as separator and root. Between
* the path segments, the hash character can also be used as a back jump
* (parent) directive. The back jump then corresponds to the number of
* additional hash characters.
*
* Note:
* Paths use lowercase letters. Upper case letters are automatically
* replaced by lower case letters when normalizing.
*
* There are four types of paths:
*
* Functional paths:
* ----
* The paths consists of three or more hash characters (###+) and are
* only temporary, they serve a function call without changing the
* current path (URL hash). If such a path is detected, the return value
* is always ###.
*
* Root paths:
* ----
* These paths are empty or contain only one hash character. The return
* value is always the given root path or # if no root path was
* specified.
*
* Relative paths:
* ----
* These paths begin without hash or begin with two or more hash (##+)
* characters. Relative paths are prepended with the passed root.
*
* Absolute paths:
* ----
* These paths begin with one hash characters. A possibly passed root is
* ignored.
*
* All paths are balanced. The directive of two or more hash characters
* is resolved, each double hash means that the preceding path segment
* is skipped. If more than two hash characters are used, it extends the
* jump length.
*
* Examples (root #x#y#z):
*
* #a#b#c#d#e##f #a#b#c#d#f
* #a#b#c#d#e###f #a#b#c#f
* ###f #x#f
* ####f #f
* empty #x#y#z
* # #x#y#z
* a#b#c #x#y#z#a#b#c
*
* Invalid roots and paths cause an error.
* The method has the following various signatures:
* function(root, path)
* function(path)
* @param root optional, otherwise # is used
* @param path to normalize (URL is also supported, only the hash is
* used here and the URL itself is ignored)
* @return the normalize path
* @throws An error occurs in the following cases:
* - if the root and/or the path is invalid
*/
normalize(...variants) {
if (variants.length <= 0)
return null;
if (variants.length > 0
&& !Object.usable(variants[0]))
return null;
if (variants.length > 1
&& !Object.usable(variants[1]))
return null;
if (variants.length > 1
&& typeof variants[0] !== "string")
throw new TypeError("Invalid root: " + typeof variants[0]);
let root = "#";
if (variants.length > 1) {
root = variants[0];
try {root = Path.normalize(root);
} catch (error) {
root = (root || "").trim();
throw new TypeError(`Invalid root${root ? ": " + root : ""}`);
}
}
if (variants.length > 1
&& typeof variants[1] !== "string")
throw new TypeError("Invalid path: " + typeof variants[1]);
if (variants.length > 0
&& typeof variants[0] !== "string")
throw new TypeError("Invalid path: " + typeof variants[0]);
let path = "";
if (variants.length === 1)
path = variants[0];
if (variants.length === 1
&& path.match(PATTERN_URL))
path = path.replace(PATTERN_URL, "$1");
else if (variants.length > 1)
path = variants[1];
path = (path || "").trim();
if (!path.match(PATTERN_PATH))
throw new TypeError(`Invalid path${String(path).trim() ? ": " + path : ""}`);
path = path.replace(/([^#])#$/, "$1");
path = path.replace(/^([^#])/, "#$1");
// Functional paths are detected.
if (path.match(PATTERN_PATH_FUNCTIONAL))
return "###";
path = root + path;
path = path.toLowerCase();
// Path will be balanced
const pattern = /#[^#]+#{2}/;
while (path.match(pattern))
path = path.replace(pattern, "#");
path = "#" + path.replace(/(^#+)|(#+)$/g, "");
return path;
}
});
})();