summaryrefslogtreecommitdiffstats
path: root/roles/reverseproxy/files/conversejs/src/shared/autocomplete
diff options
context:
space:
mode:
Diffstat (limited to 'roles/reverseproxy/files/conversejs/src/shared/autocomplete')
-rw-r--r--roles/reverseproxy/files/conversejs/src/shared/autocomplete/autocomplete.js321
-rw-r--r--roles/reverseproxy/files/conversejs/src/shared/autocomplete/component.js125
-rw-r--r--roles/reverseproxy/files/conversejs/src/shared/autocomplete/index.js10
-rw-r--r--roles/reverseproxy/files/conversejs/src/shared/autocomplete/styles/_autocomplete.scss140
-rw-r--r--roles/reverseproxy/files/conversejs/src/shared/autocomplete/suggestion.js36
-rw-r--r--roles/reverseproxy/files/conversejs/src/shared/autocomplete/utils.js89
6 files changed, 721 insertions, 0 deletions
diff --git a/roles/reverseproxy/files/conversejs/src/shared/autocomplete/autocomplete.js b/roles/reverseproxy/files/conversejs/src/shared/autocomplete/autocomplete.js
new file mode 100644
index 0000000..21eeb53
--- /dev/null
+++ b/roles/reverseproxy/files/conversejs/src/shared/autocomplete/autocomplete.js
@@ -0,0 +1,321 @@
+/**
+ * @copyright Lea Verou and the Converse.js contributors
+ * @description
+ * Started as a fork of Lea Verou's "Awesomplete"
+ * https://leaverou.github.io/awesomplete/
+ * @license Mozilla Public License (MPLv2)
+ */
+
+import { Events } from '@converse/skeletor/src/events.js';
+import { helpers, FILTER_CONTAINS, ITEM, SORT_BY_QUERY_POSITION } from './utils.js';
+import Suggestion from './suggestion.js';
+import { converse } from "@converse/headless/core";
+
+
+const u = converse.env.utils;
+
+
+export class AutoComplete {
+
+ constructor (el, config={}) {
+ this.suggestions = [];
+ this.is_opened = false;
+
+ if (u.hasClass('suggestion-box', el)) {
+ this.container = el;
+ } else {
+ this.container = el.querySelector('.suggestion-box');
+ }
+ this.input = this.container.querySelector('.suggestion-box__input');
+ this.input.setAttribute("aria-autocomplete", "list");
+
+ this.ul = this.container.querySelector('.suggestion-box__results');
+ this.status = this.container.querySelector('.suggestion-box__additions');
+
+ Object.assign(this, {
+ 'match_current_word': false, // Match only the current word, otherwise all input is matched
+ 'ac_triggers': [], // Array of keys (`ev.key`) values that will trigger auto-complete
+ 'include_triggers': [], // Array of trigger keys which should be included in the returned value
+ 'min_chars': 2,
+ 'max_items': 10,
+ 'auto_evaluate': true, // Should evaluation happen automatically without any particular key as trigger?
+ 'auto_first': false, // Should the first element be automatically selected?
+ 'data': a => a,
+ 'filter': FILTER_CONTAINS,
+ 'sort': config.sort === false ? false : SORT_BY_QUERY_POSITION,
+ 'item': ITEM
+ }, config);
+
+ this.index = -1;
+
+ this.bindEvents()
+
+ if (this.input.hasAttribute("list")) {
+ this.list = "#" + this.input.getAttribute("list");
+ this.input.removeAttribute("list");
+ } else {
+ this.list = this.input.getAttribute("data-list") || config.list || [];
+ }
+ }
+
+ bindEvents () {
+ const input = {
+ "blur": () => this.close({'reason': 'blur'})
+ }
+ if (this.auto_evaluate) {
+ input["input"] = (e) => this.evaluate(e);
+ }
+
+ this._events = {
+ 'input': input,
+ 'form': {
+ "submit": () => this.close({'reason': 'submit'})
+ },
+ 'ul': {
+ "mousedown": (ev) => this.onMouseDown(ev),
+ "mouseover": (ev) => this.onMouseOver(ev)
+ }
+ };
+ helpers.bind(this.input, this._events.input);
+ helpers.bind(this.input.form, this._events.form);
+ helpers.bind(this.ul, this._events.ul);
+ }
+
+ set list (list) {
+ if (Array.isArray(list) || typeof list === "function") {
+ this._list = list;
+ } else if (typeof list === "string" && list.includes(",")) {
+ this._list = list.split(/\s*,\s*/);
+ } else { // Element or CSS selector
+ const children = helpers.getElement(list)?.children || [];
+ this._list = Array.from(children)
+ .filter(el => !el.disabled)
+ .map(el => {
+ const text = el.textContent.trim();
+ const value = el.value || text;
+ const label = el.label || text;
+ return (value !== "") ? { label, value } : null;
+ })
+ .filter(i => i);
+ }
+
+ if (document.activeElement === this.input) {
+ this.evaluate();
+ }
+ }
+
+ get list () {
+ return this._list;
+ }
+
+ get selected () {
+ return this.index > -1;
+ }
+
+ get opened () {
+ return this.is_opened;
+ }
+
+ close (o) {
+ if (!this.opened) {
+ return;
+ }
+ this.ul.setAttribute("hidden", "");
+ this.is_opened = false;
+ this.index = -1;
+ this.trigger("suggestion-box-close", o || {});
+ }
+
+ insertValue (suggestion) {
+ if (this.match_current_word) {
+ u.replaceCurrentWord(this.input, suggestion.value);
+ } else {
+ this.input.value = suggestion.value;
+ }
+ }
+
+ open () {
+ this.ul.removeAttribute("hidden");
+ this.is_opened = true;
+
+ if (this.auto_first && this.index === -1) {
+ this.goto(0);
+ }
+ this.trigger("suggestion-box-open");
+ }
+
+ destroy () {
+ //remove events from the input and its form
+ helpers.unbind(this.input, this._events.input);
+ helpers.unbind(this.input.form, this._events.form);
+ this.input.removeAttribute("aria-autocomplete");
+ }
+
+ next () {
+ const count = this.ul.children.length;
+ this.goto(this.index < count - 1 ? this.index + 1 : (count ? 0 : -1) );
+ }
+
+ previous () {
+ const count = this.ul.children.length,
+ pos = this.index - 1;
+ this.goto(this.selected && pos !== -1 ? pos : count - 1);
+ }
+
+ goto (i, scroll=true) {
+ // Should not be used directly, highlights specific item without any checks!
+ const list = this.ul.children;
+ if (this.selected) {
+ list[this.index].setAttribute("aria-selected", "false");
+ }
+ this.index = i;
+
+ if (i > -1 && list.length > 0) {
+ list[i].setAttribute("aria-selected", "true");
+ list[i].focus();
+ this.status.textContent = list[i].textContent;
+
+ if (scroll) {
+ // scroll to highlighted element in case parent's height is fixed
+ this.ul.scrollTop = list[i].offsetTop - this.ul.clientHeight + list[i].clientHeight;
+ }
+ this.trigger("suggestion-box-highlight", {'text': this.suggestions[this.index]});
+ }
+ }
+
+ select (selected) {
+ if (selected) {
+ this.index = u.siblingIndex(selected);
+ } else {
+ selected = this.ul.children[this.index];
+ }
+ if (selected) {
+ const suggestion = this.suggestions[this.index];
+ this.insertValue(suggestion);
+ this.close({'reason': 'select'});
+ this.auto_completing = false;
+ this.trigger("suggestion-box-selectcomplete", {'text': suggestion});
+ }
+ }
+
+ onMouseOver (ev) {
+ const li = u.ancestor(ev.target, 'li');
+ if (li) {
+ const index = Array.prototype.slice.call(this.ul.children).indexOf(li);
+ this.goto(index, false);
+ }
+ }
+
+ onMouseDown (ev) {
+ if (ev.button !== 0) {
+ return; // Only select on left click
+ }
+ const li = u.ancestor(ev.target, 'li');
+ if (li) {
+ ev.preventDefault();
+ this.select(li, ev.target);
+ }
+ }
+
+ onKeyDown (ev) {
+ if (this.opened) {
+ if ([converse.keycodes.ENTER, converse.keycodes.TAB].includes(ev.keyCode) && this.selected) {
+ ev.preventDefault();
+ ev.stopPropagation();
+ this.select();
+ return true;
+ } else if (ev.keyCode === converse.keycodes.ESCAPE) {
+ this.close({'reason': 'esc'});
+ return true;
+ } else if ([converse.keycodes.UP_ARROW, converse.keycodes.DOWN_ARROW].includes(ev.keyCode)) {
+ ev.preventDefault();
+ ev.stopPropagation();
+ this[ev.keyCode === converse.keycodes.UP_ARROW ? "previous" : "next"]();
+ return true;
+ }
+ }
+
+ if ([converse.keycodes.SHIFT,
+ converse.keycodes.META,
+ converse.keycodes.META_RIGHT,
+ converse.keycodes.ESCAPE,
+ converse.keycodes.ALT
+ ].includes(ev.keyCode)) {
+
+ return;
+ }
+
+ if (this.ac_triggers.includes(ev.key)) {
+ if (ev.key === "Tab") {
+ ev.preventDefault();
+ }
+ this.auto_completing = true;
+ } else if (ev.key === "Backspace") {
+ const word = u.getCurrentWord(ev.target, ev.target.selectionEnd-1);
+ if (helpers.isMention(word, this.ac_triggers)) {
+ this.auto_completing = true;
+ }
+ }
+ }
+
+ async evaluate (ev) {
+ const selecting = this.selected && ev && (
+ ev.keyCode === converse.keycodes.UP_ARROW ||
+ ev.keyCode === converse.keycodes.DOWN_ARROW
+ );
+
+ if (!this.auto_evaluate && !this.auto_completing || selecting) {
+ return;
+ }
+
+ let value = this.match_current_word ? u.getCurrentWord(this.input) : this.input.value;
+
+ const contains_trigger = helpers.isMention(value, this.ac_triggers);
+ if (contains_trigger && !this.include_triggers.includes(ev.key)) {
+ value = u.isMentionBoundary(value[0])
+ ? value.slice('2')
+ : value.slice('1');
+ }
+
+ const is_long_enough = value.length && value.length >= this.min_chars;
+
+ if (contains_trigger || is_long_enough) {
+ this.auto_completing = true;
+
+ const list = typeof this._list === "function" ? await this._list(value) : this._list;
+ if (list.length === 0 || !this.auto_completing) {
+ this.close({'reason': 'nomatches'});
+ return;
+ }
+
+ this.index = -1;
+ this.ul.innerHTML = "";
+
+ this.suggestions = list
+ .map(item => new Suggestion(this.data(item, value), value))
+ .filter(item => this.filter(item, value));
+
+ if (this.sort !== false) {
+ this.suggestions = this.suggestions.sort(this.sort);
+ }
+ this.suggestions = this.suggestions.slice(0, this.max_items);
+ this.suggestions.forEach(text => this.ul.appendChild(this.item(text, value)));
+
+ if (this.ul.children.length === 0) {
+ this.close({'reason': 'nomatches'});
+ } else {
+ this.open();
+ }
+ } else {
+ this.close({'reason': 'nomatches'});
+ if (!contains_trigger) {
+ this.auto_completing = false;
+ }
+ }
+ }
+}
+
+// Make it an event emitter
+Object.assign(AutoComplete.prototype, Events);
+
+export default AutoComplete;
diff --git a/roles/reverseproxy/files/conversejs/src/shared/autocomplete/component.js b/roles/reverseproxy/files/conversejs/src/shared/autocomplete/component.js
new file mode 100644
index 0000000..ce9a4f4
--- /dev/null
+++ b/roles/reverseproxy/files/conversejs/src/shared/autocomplete/component.js
@@ -0,0 +1,125 @@
+import AutoComplete from './autocomplete.js';
+import { CustomElement } from 'shared/components/element.js';
+import { FILTER_CONTAINS, FILTER_STARTSWITH } from './utils.js';
+import { api } from '@converse/headless/core';
+import { html } from 'lit';
+
+/**
+ * A custom element that can be used to add auto-completion suggestions to a form input.
+ * @class AutoCompleteComponent
+ *
+ * @property { "above" | "below" } [position="above"]
+ * Should the autocomplete list show above or below the input element?
+ * @property { Boolean } [autofocus=false]
+ * Should the `focus` attribute be set on the input element?
+ * @property { Function } getAutoCompleteList
+ * A function that returns the list of autocomplete suggestions
+ * @property { Array } list
+ * An array of suggestions, to be used instead of the `getAutoCompleteList` * function
+ * @property { Boolean } [auto_evaluate=true]
+ * Should evaluation happen automatically without any particular key as trigger?
+ * @property { Boolean } [auto_first=false]
+ * Should the first element automatically be selected?
+ * @property { "contains" | "startswith" } [filter="contains"]
+ * Provide matches which contain the entered text, or which starts with the entered text
+ * @property { String } [include_triggers=""]
+ * Space separated characters which should be included in the returned value
+ * @property { Number } [min_chars=1]
+ * The minimum number of characters to be entered into the input before autocomplete starts.
+ * @property { String } [name]
+ * The `name` attribute of the `input` element
+ * @property { String } [placeholder]
+ * The `placeholder` attribute of the `input` element
+ * @property { String } [triggers]
+ * String of space separated characters which trigger autocomplete
+ *
+ * @example
+ * <converse-autocomplete
+ * .getAutoCompleteList="${getAutoCompleteList}"
+ * placeholder="${placeholder_text}"
+ * name="foo">
+ * </converse-autocomplete>
+ */
+export default class AutoCompleteComponent extends CustomElement {
+ static get properties () {
+ return {
+ 'position': { type: String },
+ 'autofocus': { type: Boolean },
+ 'getAutoCompleteList': { type: Function },
+ 'list': { type: Array },
+ 'auto_evaluate': { type: Boolean },
+ 'auto_first': { type: Boolean },
+ 'filter': { type: String },
+ 'include_triggers': { type: String },
+ 'min_chars': { type: Number },
+ 'name': { type: String },
+ 'placeholder': { type: String },
+ 'triggers': { type: String },
+ 'required': { type: Boolean },
+ };
+ }
+
+ constructor () {
+ super();
+ this.position = 'above';
+ this.auto_evaluate = true;
+ this.auto_first = false;
+ this.filter = 'contains';
+ this.include_triggers = '';
+ this.match_current_word = false; // Match only the current word, otherwise all input is matched
+ this.max_items = 10;
+ this.min_chars = 1;
+ this.triggers = '';
+ }
+
+ render () {
+ const position_class = `suggestion-box__results--${this.position}`;
+ return html`
+ <div class="suggestion-box suggestion-box__name">
+ <ul class="suggestion-box__results ${position_class}" hidden=""></ul>
+ <input
+ ?autofocus=${this.autofocus}
+ ?required=${this.required}
+ type="text"
+ name="${this.name}"
+ autocomplete="off"
+ @keydown=${this.onKeyDown}
+ @keyup=${this.onKeyUp}
+ class="form-control suggestion-box__input"
+ placeholder="${this.placeholder}"
+ />
+ <span
+ class="suggestion-box__additions visually-hidden"
+ role="status"
+ aria-live="assertive"
+ aria-relevant="additions"
+ ></span>
+ </div>
+ `;
+ }
+
+ firstUpdated () {
+ this.auto_complete = new AutoComplete(this.firstElementChild, {
+ 'ac_triggers': this.triggers.split(' '),
+ 'auto_evaluate': this.auto_evaluate,
+ 'auto_first': this.auto_first,
+ 'filter': this.filter == 'contains' ? FILTER_CONTAINS : FILTER_STARTSWITH,
+ 'include_triggers': [],
+ 'list': this.list ?? ((q) => this.getAutoCompleteList(q)),
+ 'match_current_word': true,
+ 'max_items': this.max_items,
+ 'min_chars': this.min_chars,
+ });
+ this.auto_complete.on('suggestion-box-selectcomplete', () => (this.auto_completing = false));
+ }
+
+ onKeyDown (ev) {
+ this.auto_complete.onKeyDown(ev);
+ }
+
+ onKeyUp (ev) {
+ this.auto_complete.evaluate(ev);
+ }
+}
+
+api.elements.define('converse-autocomplete', AutoCompleteComponent);
diff --git a/roles/reverseproxy/files/conversejs/src/shared/autocomplete/index.js b/roles/reverseproxy/files/conversejs/src/shared/autocomplete/index.js
new file mode 100644
index 0000000..4a80fc0
--- /dev/null
+++ b/roles/reverseproxy/files/conversejs/src/shared/autocomplete/index.js
@@ -0,0 +1,10 @@
+import './component.js';
+import AutoComplete from './autocomplete.js';
+import { FILTER_CONTAINS, FILTER_STARTSWITH } from './utils.js';
+import { _converse } from '@converse/headless/core';
+
+import './styles/_autocomplete.scss';
+
+_converse.FILTER_CONTAINS = FILTER_CONTAINS;
+_converse.FILTER_STARTSWITH = FILTER_STARTSWITH;
+_converse.AutoComplete = AutoComplete;
diff --git a/roles/reverseproxy/files/conversejs/src/shared/autocomplete/styles/_autocomplete.scss b/roles/reverseproxy/files/conversejs/src/shared/autocomplete/styles/_autocomplete.scss
new file mode 100644
index 0000000..78b330d
--- /dev/null
+++ b/roles/reverseproxy/files/conversejs/src/shared/autocomplete/styles/_autocomplete.scss
@@ -0,0 +1,140 @@
+.conversejs {
+ [hidden] { display: none; }
+
+ .visually-hidden {
+ position: absolute;
+ clip: rect(0, 0, 0, 0);
+ }
+
+ .form-group {
+ .suggestion-box {
+ width: 100%;
+ }
+ }
+
+ .suggestion-box {
+ position: relative;
+ mark {
+ background: var(--completion-light-color);
+ }
+
+ > input {
+ display: block;
+ }
+
+ .suggestion-box__results,
+ > ul {
+ &:before {
+ content: "";
+ position: absolute;
+ top: -.43em;
+ left: 1em;
+ width: 0; height: 0;
+ padding: .4em;
+ background: var(--background);
+ border: inherit;
+ border-right: 0;
+ border-bottom: 0;
+ -webkit-transform: rotate(45deg);
+ transform: rotate(45deg);
+ z-index: -1;
+ }
+ border-radius: .3em;
+ border: 1px solid var(--focus-color);
+ box-shadow: .05em .2em .6em rgba(0,0,0,.1);
+ box-sizing: border-box;
+ left: 0;
+ list-style: none;
+ margin: .2em 0 0;
+ min-width: 100%;
+ padding: 0;
+ position: absolute;
+ right: 0;
+ text-shadow: none;
+ z-index: 2;
+
+ > li {
+ background: var(--background);
+ color: var(--text-color);
+ cursor: pointer;
+ display: flex;
+ overflow-x: hidden;
+ padding: 1em;
+ position: relative;
+ text-overflow: ellipsis;
+ }
+ }
+ .suggestion-box__results--below {
+ top: 3em;
+ }
+ .suggestion-box__results--above {
+ bottom: 4.5em;
+ &:before {
+ display: none;
+ }
+ &:after {
+ z-index: -1;
+ content: "";
+ position: absolute;
+ bottom: -0.43em;
+ left: 1em;
+ width: 0;
+ height: 0;
+ padding: 0.4em;
+ background: var(--background);
+ border: inherit;
+ border-left: 0;
+ border-top: 0;
+ -webkit-transform: rotate(45deg);
+ transform: rotate(45deg);
+ }
+ }
+ }
+
+ .suggestion-box > ul[hidden],
+ .suggestion-box > ul:empty {
+ display: none;
+ }
+
+ @supports (transform: scale(0)) {
+ .suggestion-box > ul {
+ transition: .3s cubic-bezier(.4,.2,.5,1.4);
+ transform-origin: 1.43em -.43em;
+ }
+
+ .suggestion-box > ul[hidden],
+ .suggestion-box > ul:empty {
+ opacity: 0;
+ transform: scale(0);
+ display: block;
+ transition-timing-function: ease;
+ }
+ }
+
+ .suggestion-box > ul > li[aria-selected="true"] {
+ background: var(--completion-dark-color);
+ color: var(--inverse-link-color);
+ }
+
+ .suggestion-box li:hover mark {
+ background: var(--completion-light-color);
+ color: var(--inverse-link-color);
+ }
+
+ .suggestion-box li[aria-selected="true"] mark {
+ background: var(--completion-normal-color);
+ color: inherit;
+ }
+}
+
+.conversejs.converse-fullscreen {
+ .suggestion-box__results--above {
+ bottom: 4.5em;
+ }
+}
+
+.conversejs.converse-overlayed {
+ .suggestion-box__results--above {
+ bottom: 3.5em;
+ }
+}
diff --git a/roles/reverseproxy/files/conversejs/src/shared/autocomplete/suggestion.js b/roles/reverseproxy/files/conversejs/src/shared/autocomplete/suggestion.js
new file mode 100644
index 0000000..40bd132
--- /dev/null
+++ b/roles/reverseproxy/files/conversejs/src/shared/autocomplete/suggestion.js
@@ -0,0 +1,36 @@
+/**
+ * An autocomplete suggestion
+ */
+class Suggestion extends String {
+ /**
+ * @param { Any } data - The auto-complete data. Ideally an object e.g. { label, value },
+ * which specifies the value and human-presentable label of the suggestion.
+ * @param { string } query - The query string being auto-completed
+ */
+ constructor (data, query) {
+ super();
+ const o = Array.isArray(data)
+ ? { label: data[0], value: data[1] }
+ : typeof data === 'object' && 'label' in data && 'value' in data
+ ? data
+ : { label: data, value: data };
+
+ this.label = o.label || o.value;
+ this.value = o.value;
+ this.query = query;
+ }
+
+ get lenth () {
+ return this.label.length;
+ }
+
+ toString () {
+ return '' + this.label;
+ }
+
+ valueOf () {
+ return this.toString();
+ }
+}
+
+export default Suggestion;
diff --git a/roles/reverseproxy/files/conversejs/src/shared/autocomplete/utils.js b/roles/reverseproxy/files/conversejs/src/shared/autocomplete/utils.js
new file mode 100644
index 0000000..af89268
--- /dev/null
+++ b/roles/reverseproxy/files/conversejs/src/shared/autocomplete/utils.js
@@ -0,0 +1,89 @@
+import { converse } from '@converse/headless/core';
+
+const u = converse.env.utils;
+
+export const helpers = {
+ getElement (expr, el) {
+ return typeof expr === 'string' ? (el || document).querySelector(expr) : expr || null;
+ },
+
+ bind (element, o) {
+ if (element) {
+ for (var event in o) {
+ if (!Object.prototype.hasOwnProperty.call(o, event)) {
+ continue;
+ }
+ const callback = o[event];
+ event.split(/\s+/).forEach(event => element.addEventListener(event, callback));
+ }
+ }
+ },
+
+ unbind (element, o) {
+ if (element) {
+ for (var event in o) {
+ if (!Object.prototype.hasOwnProperty.call(o, event)) {
+ continue;
+ }
+ const callback = o[event];
+ event.split(/\s+/).forEach(event => element.removeEventListener(event, callback));
+ }
+ }
+ },
+
+ regExpEscape (s) {
+ return s.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&');
+ },
+
+ isMention (word, ac_triggers) {
+ return (
+ ac_triggers.includes(word[0]) ||
+ (u.isMentionBoundary(word[0]) && ac_triggers.includes(word[1]))
+ );
+ }
+};
+
+export const FILTER_CONTAINS = function (text, input) {
+ return RegExp(helpers.regExpEscape(input.trim()), 'i').test(text);
+};
+
+export const FILTER_STARTSWITH = function (text, input) {
+ return RegExp('^' + helpers.regExpEscape(input.trim()), 'i').test(text);
+};
+
+const SORT_BY_LENGTH = function (a, b) {
+ if (a.length !== b.length) {
+ return a.length - b.length;
+ }
+ return a < b ? -1 : 1;
+};
+
+export const SORT_BY_QUERY_POSITION = function (a, b) {
+ const query = a.query.toLowerCase();
+ const x = a.label.toLowerCase().indexOf(query);
+ const y = b.label.toLowerCase().indexOf(query);
+
+ if (x === y) {
+ return SORT_BY_LENGTH(a, b);
+ }
+ return (x === -1 ? Infinity : x) < (y === -1 ? Infinity : y) ? -1 : 1;
+};
+
+export const ITEM = (text, input) => {
+ input = input.trim();
+ const element = document.createElement('li');
+ element.setAttribute('aria-selected', 'false');
+
+ const regex = new RegExp('(' + input + ')', 'ig');
+ const parts = input ? text.split(regex) : [text];
+ parts.forEach(txt => {
+ if (input && txt.match(regex)) {
+ const match = document.createElement('mark');
+ match.textContent = txt;
+ element.appendChild(match);
+ } else {
+ element.appendChild(document.createTextNode(txt));
+ }
+ });
+ return element;
+};