瀏覽代碼

Move unnecessary templates in headless package to main package

JC Brand 6 年之前
父節點
當前提交
7590a030b0

+ 2 - 0
src/converse-message-view.js

@@ -6,6 +6,7 @@
 
 (function (root, factory) {
     define([
+        "./utils/html",
         "utils/emoji",
         "@converse/headless/converse-core",
         "xss",
@@ -17,6 +18,7 @@
         "templates/message_versions_modal.html",
     ], factory);
 }(this, function (
+        html,
         u,
         converse,
         xss,

+ 2 - 2
src/converse-register.js

@@ -12,12 +12,12 @@
 (function (root, factory) {
     define(["utils/form",
             "@converse/headless/converse-core",
-            "@converse/headless/templates/form_username.html",
+            "templates/form_username.html",
             "templates/register_link.html",
             "templates/register_panel.html",
             "templates/registration_form.html",
             "templates/registration_request.html",
-            "@converse/headless/templates/form_input.html",
+            "templates/form_input.html",
             "templates/spinner.html",
             "converse-controlbox"
     ], factory);

+ 2 - 485
src/headless/utils/core.js

@@ -12,15 +12,9 @@
         define([
             "sizzle",
             "es6-promise/dist/es6-promise.auto",
-            "fast-text-encoding/text",
             "../lodash.noconflict",
             "backbone",
             "strophe.js",
-            "urijs",
-            "../templates/audio.html",
-            "../templates/file.html",
-            "../templates/image.html",
-            "../templates/video.html"
         ], factory);
     } else {
         // Used by the mockups
@@ -49,57 +43,14 @@
 }(this, function (
         sizzle,
         Promise,
-        FastTextEncoding,
         _,
         Backbone,
-        Strophe,
-        URI,
-        tpl_audio,
-        tpl_file,
-        tpl_image,
-        tpl_video
+        Strophe
     ) {
     "use strict";
     Strophe = Strophe.Strophe;
 
-    const URL_REGEX = /\b(https?:\/\/|www\.|https?:\/\/www\.)[^\s<>]{2,200}\b\/?/g;
-
-    const logger = _.assign({
-        'debug': _.get(console, 'log') ? console.log.bind(console) : _.noop,
-        'error': _.get(console, 'log') ? console.log.bind(console) : _.noop,
-        'info': _.get(console, 'log') ? console.log.bind(console) : _.noop,
-        'warn': _.get(console, 'log') ? console.log.bind(console) : _.noop
-    }, console);
-
-    const isImage = function (url) {
-        return new Promise((resolve, reject) => {
-            var img = new Image();
-            var timer = window.setTimeout(function () {
-                reject(new Error("Could not determine whether it's an image"));
-                img = null;
-            }, 3000);
-            img.onerror = img.onabort = function () {
-                clearTimeout(timer);
-                reject(new Error("Could not determine whether it's an image"));
-            };
-            img.onload = function () {
-                clearTimeout(timer);
-                resolve(img);
-            };
-            img.src = url;
-        });
-    };
-
-    function slideOutWrapup (el) {
-        /* Wrapup function for slideOut. */
-        el.removeAttribute('data-slider-marker');
-        el.classList.remove('collapsed');
-        el.style.overflow = "";
-        el.style.height = "";
-    }
-
-
-    var u = {};
+    const u = {};
 
     u.getLongestSubstring = function (string, candidates) {
         function reducer (accumulator, current_value) {
@@ -116,118 +67,6 @@
         return candidates.reduce(reducer, '');
     }
 
-    u.getNextElement = function (el, selector='*') {
-        let next_el = el.nextElementSibling;
-        while (!_.isNull(next_el) && !sizzle.matchesSelector(next_el, selector)) {
-            next_el = next_el.nextElementSibling;
-        }
-        return next_el;
-    }
-
-    u.getPreviousElement = function (el, selector='*') {
-        let prev_el = el.previousSibling;
-        while (!_.isNull(prev_el) && !sizzle.matchesSelector(prev_el, selector)) {
-            prev_el = prev_el.previousSibling
-        }
-        return prev_el;
-    }
-
-    u.getFirstChildElement = function (el, selector='*') {
-        let first_el = el.firstElementChild;
-        while (!_.isNull(first_el) && !sizzle.matchesSelector(first_el, selector)) {
-            first_el = first_el.nextSibling
-        }
-        return first_el;
-    }
-
-    u.getLastChildElement = function (el, selector='*') {
-        let last_el = el.lastElementChild;
-        while (!_.isNull(last_el) && !sizzle.matchesSelector(last_el, selector)) {
-            last_el = last_el.previousSibling
-        }
-        return last_el;
-    }
-
-    u.calculateElementHeight = function (el) {
-        /* Return the height of the passed in DOM element,
-         * based on the heights of its children.
-         */
-        return _.reduce(
-            el.children,
-            (result, child) => result + child.offsetHeight, 0
-        );
-    }
-
-    u.addClass = function (className, el) {
-        if (el instanceof Element) {
-            el.classList.add(className);
-        }
-    }
-
-    u.removeClass = function (className, el) {
-        if (el instanceof Element) {
-            el.classList.remove(className);
-        }
-        return el;
-    }
-
-    u.removeElement = function (el) {
-        if (!_.isNil(el) && !_.isNil(el.parentNode)) {
-            el.parentNode.removeChild(el);
-        }
-    }
-
-    u.showElement = _.flow(
-        _.partial(u.removeClass, 'collapsed'),
-        _.partial(u.removeClass, 'hidden')
-    )
-
-    u.hideElement = function (el) {
-        if (!_.isNil(el)) {
-            el.classList.add('hidden');
-        }
-        return el;
-    }
-
-    u.ancestor = function (el, selector) {
-        let parent = el;
-        while (!_.isNil(parent) && !sizzle.matchesSelector(parent, selector)) {
-            parent = parent.parentElement;
-        }
-        return parent;
-    }
-
-    u.nextUntil = function (el, selector, include_self=false) {
-        /* Return the element's siblings until one matches the selector. */
-        const matches = [];
-        let sibling_el = el.nextElementSibling;
-        while (!_.isNil(sibling_el) && !sibling_el.matches(selector)) {
-            matches.push(sibling_el);
-            sibling_el = sibling_el.nextElementSibling;
-        }
-        return matches;
-    }
-
-    u.unescapeHTML = function (string) {
-        /* Helper method that replace HTML-escaped symbols with equivalent characters
-         * (e.g. transform occurrences of '&amp;' to '&')
-         *
-         * Parameters:
-         *  (String) string: a String containing the HTML-escaped symbols.
-         */
-        var div = document.createElement('div');
-        div.innerHTML = string;
-        return div.innerText;
-    };
-
-    u.escapeHTML = function (string) {
-        return string
-            .replace(/&/g, "&amp;")
-            .replace(/</g, "&lt;")
-            .replace(/>/g, "&gt;")
-            .replace(/"/g, "&quot;");
-    };
-
     u.prefixMentions = function (message) {
         /* Given a message object, return its text with @ chars
          * inserted before the mentioned nicknames.
@@ -241,328 +80,6 @@
         return text;
     };
 
-    u.addMentionsMarkup = function (text, references, chatbox) {
-        if (chatbox.get('message_type') !== 'groupchat') {
-            return text;
-        }
-        const nick = chatbox.get('nick');
-        references
-            .sort((a, b) => b.begin - a.begin)
-            .forEach(ref => {
-                const mention = text.slice(ref.begin, ref.end)
-                chatbox;
-                if (mention === nick) {
-                    text = text.slice(0, ref.begin) + `<span class="mention mention--self badge badge-info">${mention}</span>` + text.slice(ref.end);
-                } else {
-                    text = text.slice(0, ref.begin) + `<span class="mention">${mention}</span>` + text.slice(ref.end);
-                }
-            });
-        return text;
-    };
-
-    u.addHyperlinks = function (text) {
-        return URI.withinString(text, url => {
-            const uri = new URI(url);
-            url = uri.normalize()._string;
-            const pretty_url = uri._parts.urn ? url : uri.readable();
-            if (!uri._parts.protocol && !url.startsWith('http://') && !url.startsWith('https://')) {
-                url = 'http://' + url;
-            }
-            if (uri._parts.protocol === 'xmpp' && uri._parts.query === 'join') {
-                return `<a target="_blank" rel="noopener" class="open-chatroom" href="${url}">${u.escapeHTML(pretty_url)}</a>`;
-            }
-            return `<a target="_blank" rel="noopener" href="${url}">${u.escapeHTML(pretty_url)}</a>`;
-        }, {
-            'start': /\b(?:([a-z][a-z0-9.+-]*:\/\/)|xmpp:|mailto:|www\.)/gi
-        });
-    };
-
-    u.renderNewLines = function (text) {
-        return text.replace(/\n\n+/g, '<br/><br/>').replace(/\n/g, '<br/>');
-    };
-
-    u.renderImageURLs = function (_converse, el) {
-        /* Returns a Promise which resolves once all images have been loaded.
-         */
-        if (!_converse.show_images_inline) {
-            return Promise.resolve();
-        }
-        const { __ } = _converse;
-        const list = el.textContent.match(URL_REGEX) || [];
-        return Promise.all(
-            _.map(list, url =>
-                new Promise((resolve, reject) => {
-                    if (u.isImageURL(url)) {
-                        return isImage(url).then(img => {
-                            const i = new Image();
-                            i.src = img.src;
-                            i.addEventListener('load', resolve);
-                            // We also resolve for non-images, otherwise the
-                            // Promise.all resolves prematurely.
-                            i.addEventListener('error', resolve);
-
-                            const { __ } = _converse;
-                            _.each(sizzle(`a[href="${url}"]`, el), (a) => {
-                                a.outerHTML= tpl_image({
-                                    'url': url,
-                                    'label_download': __('Download')
-                                })
-                            });
-                        }).catch(resolve)
-                    } else {
-                        return resolve();
-                    }
-                })
-            )
-        )
-    };
-
-    u.renderFileURL = function (_converse, url) {
-        const uri = new URI(url);
-        if (u.isImageURL(uri) || u.isVideoURL(uri) || u.isAudioURL(uri)) {
-            return url;
-        }
-        const { __ } = _converse,
-              filename = uri.filename();
-        return tpl_file({
-            'url': url,
-            'label_download': __('Download file "%1$s"', decodeURI(filename))
-        })
-    };
-
-    u.isAudioURL = function (url) {
-        if (!(url instanceof URI)) {
-            url = new URI(url);
-        }
-        const filename = url.filename().toLowerCase();
-        if (!_.includes(["https", "http"], url.protocol().toLowerCase())) {
-            return false;
-        }
-        return filename.endsWith('.ogg') || filename.endsWith('.mp3') || filename.endsWith('.m4a');
-    }
-
-    u.isVideoURL = function (url) {
-        if (!(url instanceof URI)) {
-            url = new URI(url);
-        }
-        const filename = url.filename().toLowerCase();
-        if (!_.includes(["https", "http"], url.protocol().toLowerCase())) {
-            return false;
-        }
-        return filename.endsWith('.mp4') || filename.endsWith('.webm');
-    }
-
-    u.isImageURL = function (url) {
-        if (!(url instanceof URI)) {
-            url = new URI(url);
-        }
-        const filename = url.filename().toLowerCase();
-        if (!_.includes(["https", "http"], url.protocol().toLowerCase())) {
-            return false;
-        }
-        return filename.endsWith('.jpg') || filename.endsWith('.jpeg') ||
-               filename.endsWith('.png') || filename.endsWith('.gif') ||
-               filename.endsWith('.bmp') || filename.endsWith('.tiff') ||
-               filename.endsWith('.svg');
-    };
-
-    u.renderImageURL = function (_converse, url) {
-        if (!_converse.show_images_inline) {
-            return u.addHyperlinks(url);
-        }
-        const uri = new URI(url);
-        if (u.isImageURL(uri)) {
-            const { __ } = _converse;
-            return tpl_image({
-                'url': url,
-                'label_download': __('Download image "%1$s"', decodeURI(uri.filename()))
-            })
-        }
-        return url;
-    };
-
-    u.renderMovieURL = function (_converse, url) {
-        const uri = new URI(url);
-        if (u.isVideoURL(uri)) {
-            const { __ } = _converse;
-            return tpl_video({
-                'url': url,
-                'label_download': __('Download video file "%1$s"', decodeURI(uri.filename()))
-            })
-        }
-        return url;
-    };
-
-    u.renderAudioURL = function (_converse, url) {
-        const uri = new URI(url);
-        if (u.isAudioURL(uri)) {
-            const { __ } = _converse;
-            return tpl_audio({
-                'url': url,
-                'label_download': __('Download audio file "%1$s"', decodeURI(uri.filename()))
-            })
-        }
-        return url;
-    };
-
-    u.slideInAllElements = function (elements, duration=300) {
-        return Promise.all(
-            _.map(
-                elements,
-                _.partial(u.slideIn, _, duration)
-            ));
-    };
-
-    u.slideToggleElement = function (el, duration) {
-        if (_.includes(el.classList, 'collapsed') ||
-                _.includes(el.classList, 'hidden')) {
-            return u.slideOut(el, duration);
-        } else {
-            return u.slideIn(el, duration);
-        }
-    };
-
-    u.hasClass = function (className, el) {
-        return _.includes(el.classList, className);
-    };
-
-    u.slideOut = function (el, duration=200) {
-        /* Shows/expands an element by sliding it out of itself
-         *
-         * Parameters:
-         *      (HTMLElement) el - The HTML string
-         *      (Number) duration - The duration amount in milliseconds
-         */
-        return new Promise((resolve, reject) => {
-            if (_.isNil(el)) {
-                const err = "Undefined or null element passed into slideOut"
-                logger.warn(err);
-                reject(new Error(err));
-                return;
-            }
-            const marker = el.getAttribute('data-slider-marker');
-            if (marker) {
-                el.removeAttribute('data-slider-marker');
-                window.cancelAnimationFrame(marker);
-            }
-            const end_height = u.calculateElementHeight(el);
-            if (window.converse_disable_effects) { // Effects are disabled (for tests)
-                el.style.height = end_height + 'px';
-                slideOutWrapup(el);
-                resolve();
-                return;
-            }
-            if (!u.hasClass('collapsed', el) && !u.hasClass('hidden', el)) {
-                resolve();
-                return;
-            }
-
-            const steps = duration/17; // We assume 17ms per animation which is ~60FPS
-            let height = 0;
-
-            function draw () {
-                height += end_height/steps;
-                if (height < end_height) {
-                    el.style.height = height + 'px';
-                    el.setAttribute(
-                        'data-slider-marker',
-                        window.requestAnimationFrame(draw)
-                    );
-                } else {
-                    // We recalculate the height to work around an apparent
-                    // browser bug where browsers don't know the correct
-                    // offsetHeight beforehand.
-                    el.removeAttribute('data-slider-marker');
-                    el.style.height = u.calculateElementHeight(el) + 'px';
-                    el.style.overflow = "";
-                    el.style.height = "";
-                    resolve();
-                }
-            }
-            el.style.height = '0';
-            el.style.overflow = 'hidden';
-            el.classList.remove('hidden');
-            el.classList.remove('collapsed');
-            el.setAttribute(
-                'data-slider-marker',
-                window.requestAnimationFrame(draw)
-            );
-        });
-    };
-
-    u.slideIn = function (el, duration=200) {
-        /* Hides/collapses an element by sliding it into itself. */
-        return new Promise((resolve, reject) => {
-            if (_.isNil(el)) {
-                const err = "Undefined or null element passed into slideIn";
-                logger.warn(err);
-                return reject(new Error(err));
-            } else if (_.includes(el.classList, 'collapsed')) {
-                return resolve(el);
-            } else if (window.converse_disable_effects) { // Effects are disabled (for tests)
-                el.classList.add('collapsed');
-                el.style.height = "";
-                return resolve(el);
-            }
-            const marker = el.getAttribute('data-slider-marker');
-            if (marker) {
-                el.removeAttribute('data-slider-marker');
-                window.cancelAnimationFrame(marker);
-            }
-            const original_height = el.offsetHeight,
-                 steps = duration/17; // We assume 17ms per animation which is ~60FPS
-            let height = original_height;
-
-            el.style.overflow = 'hidden';
-
-            function draw () {
-                height -= original_height/steps;
-                if (height > 0) {
-                    el.style.height = height + 'px';
-                    el.setAttribute(
-                        'data-slider-marker',
-                        window.requestAnimationFrame(draw)
-                    );
-                } else {
-                    el.removeAttribute('data-slider-marker');
-                    el.classList.add('collapsed');
-                    el.style.height = "";
-                    resolve(el);
-                }
-            }
-            el.setAttribute(
-                'data-slider-marker',
-                window.requestAnimationFrame(draw)
-            );
-        });
-    };
-
-    function afterAnimationEnds (el, callback) {
-        el.classList.remove('visible');
-        if (_.isFunction(callback)) {
-            callback();
-        }
-    }
-
-    u.fadeIn = function (el, callback) {
-        if (_.isNil(el)) {
-            logger.warn("Undefined or null element passed into fadeIn");
-        }
-        if (window.converse_disable_effects) {
-            el.classList.remove('hidden');
-            return afterAnimationEnds(el, callback);
-        }
-        if (_.includes(el.classList, 'hidden')) {
-            el.classList.add('visible');
-            el.classList.remove('hidden');
-            el.addEventListener("webkitAnimationEnd", _.partial(afterAnimationEnds, el, callback));
-            el.addEventListener("animationend", _.partial(afterAnimationEnds, el, callback));
-            el.addEventListener("oanimationend", _.partial(afterAnimationEnds, el, callback));
-        } else {
-            afterAnimationEnds(el, callback);
-        }
-    };
-
     u.isValidJID = function (jid) {
         return _.compact(jid.split('@')).length === 2 && !jid.startsWith('@') && !jid.endsWith('@');
     };

+ 3 - 132
src/headless/utils/form.js

@@ -6,49 +6,16 @@
 // Copyright (c) 2013-2018, Jan-Carel Brand <jc@opkode.com>
 // Licensed under the Mozilla Public License (MPLv2)
 //
-/*global define, escape, Jed */
+/*global define */
 (function (root, factory) {
     define([
-        "sizzle",
         "../lodash.noconflict",
         "./core",
-        "../templates/field.html",
-        "../templates/select_option.html",
-        "../templates/form_select.html",
-        "../templates/form_textarea.html",
-        "../templates/form_checkbox.html",
-        "../templates/form_username.html",
-        "../templates/form_input.html",
-        "../templates/form_captcha.html",
-        "../templates/form_url.html",
+        "../templates/field.html"
     ], factory);
-}(this, function (
-        sizzle,
-        _,
-        u,
-        tpl_field,
-        tpl_select_option,
-        tpl_form_select,
-        tpl_form_textarea,
-        tpl_form_checkbox,
-        tpl_form_username,
-        tpl_form_input,
-        tpl_form_captcha,
-        tpl_form_url
-    ) {
+}(this, function (_, u, tpl_field) {
     "use strict";
 
-    var XFORM_TYPE_MAP = {
-        'text-private': 'password',
-        'text-single': 'text',
-        'fixed': 'label',
-        'boolean': 'checkbox',
-        'hidden': 'hidden',
-        'jid-multi': 'textarea',
-        'list-single': 'dropdown',
-        'list-multi': 'dropdown'
-    };
-
     u.webForm2xForm = function (field) {
         /* Takes an HTML DOM and turns it into an XForm field.
          *
@@ -72,101 +39,5 @@
             })
         );
     };
-
-    u.xForm2webForm = function (field, stanza, domain) {
-        /* Takes a field in XMPP XForm (XEP-004: Data Forms) format
-         * and turns it into an HTML field.
-         *
-         * Returns either text or a DOM element (which is not ideal, but fine
-         * for now).
-         *
-         *  Parameters:
-         *      (XMLElement) field - the field to convert
-         */
-        if (field.getAttribute('type')) {
-            if (field.getAttribute('type') === 'list-single' ||
-                field.getAttribute('type') === 'list-multi') {
-
-                const values = _.map(
-                    u.queryChildren(field, 'value'),
-                    _.partial(_.get, _, 'textContent')
-                );
-                const options = _.map(
-                    u.queryChildren(field, 'option'),
-                    function (option) {
-                        const value = _.get(option.querySelector('value'), 'textContent');
-                        return tpl_select_option({
-                            'value': value,
-                            'label': option.getAttribute('label'),
-                            'selected': _.includes(values, value),
-                            'required': !_.isNil(field.querySelector('required'))
-                        })
-                    }
-                );
-                return tpl_form_select({
-                    'id': u.getUniqueId(),
-                    'name': field.getAttribute('var'),
-                    'label': field.getAttribute('label'),
-                    'options': options.join(''),
-                    'multiple': (field.getAttribute('type') === 'list-multi'),
-                    'required': !_.isNil(field.querySelector('required'))
-                });
-            } else if (field.getAttribute('type') === 'fixed') {
-                const text = _.get(field.querySelector('value'), 'textContent');
-                return '<p class="form-help">'+text+'</p>';
-            } else if (field.getAttribute('type') === 'jid-multi') {
-                return tpl_form_textarea({
-                    'name': field.getAttribute('var'),
-                    'label': field.getAttribute('label') || '',
-                    'value': _.get(field.querySelector('value'), 'textContent'),
-                    'required': !_.isNil(field.querySelector('required'))
-                });
-            } else if (field.getAttribute('type') === 'boolean') {
-                return tpl_form_checkbox({
-                    'id': u.getUniqueId(),
-                    'name': field.getAttribute('var'),
-                    'label': field.getAttribute('label') || '',
-                    'checked': _.get(field.querySelector('value'), 'textContent') === "1" && 'checked="1"' || '',
-                    'required': !_.isNil(field.querySelector('required'))
-                });
-            } else if (field.getAttribute('var') === 'url') {
-                return tpl_form_url({
-                    'label': field.getAttribute('label') || '',
-                    'value': _.get(field.querySelector('value'), 'textContent')
-                });
-            } else if (field.getAttribute('var') === 'username') {
-                return tpl_form_username({
-                    'domain': ' @'+domain,
-                    'name': field.getAttribute('var'),
-                    'type': XFORM_TYPE_MAP[field.getAttribute('type')],
-                    'label': field.getAttribute('label') || '',
-                    'value': _.get(field.querySelector('value'), 'textContent'),
-                    'required': !_.isNil(field.querySelector('required'))
-                });
-            } else {
-                return tpl_form_input({
-                    'id': u.getUniqueId(),
-                    'label': field.getAttribute('label') || '',
-                    'name': field.getAttribute('var'),
-                    'placeholder': null,
-                    'required': !_.isNil(field.querySelector('required')),
-                    'type': XFORM_TYPE_MAP[field.getAttribute('type')],
-                    'value': _.get(field.querySelector('value'), 'textContent')
-                });
-            }
-        } else {
-            if (field.getAttribute('var') === 'ocr') { // Captcha
-                const uri = field.querySelector('uri');
-                const el = sizzle('data[cid="'+uri.textContent.replace(/^cid:/, '')+'"]', stanza)[0];
-                return tpl_form_captcha({
-                    'label': field.getAttribute('label'),
-                    'name': field.getAttribute('var'),
-                    'data': _.get(el, 'textContent'),
-                    'type': uri.getAttribute('type'),
-                    'required': !_.isNil(field.querySelector('required'))
-                });
-            }
-        }
-    }
     return u;
 }));

+ 0 - 0
src/headless/templates/audio.html → src/templates/audio.html


+ 0 - 0
src/headless/templates/file.html → src/templates/file.html


+ 0 - 0
src/headless/templates/form_captcha.html → src/templates/form_captcha.html


+ 0 - 0
src/headless/templates/form_checkbox.html → src/templates/form_checkbox.html


+ 0 - 0
src/headless/templates/form_input.html → src/templates/form_input.html


+ 0 - 0
src/headless/templates/form_select.html → src/templates/form_select.html


+ 0 - 0
src/headless/templates/form_textarea.html → src/templates/form_textarea.html


+ 0 - 0
src/headless/templates/form_url.html → src/templates/form_url.html


+ 0 - 0
src/headless/templates/form_username.html → src/templates/form_username.html


+ 0 - 0
src/headless/templates/image.html → src/templates/image.html


+ 0 - 0
src/headless/templates/select_option.html → src/templates/select_option.html


+ 0 - 0
src/headless/templates/video.html → src/templates/video.html


+ 643 - 0
src/utils/html.js

@@ -0,0 +1,643 @@
+// Converse.js (A browser based XMPP chat client)
+// http://conversejs.org
+//
+// This is a form utilities module.
+//
+// Copyright (c) 2013-2018, Jan-Carel Brand <jc@opkode.com>
+// Licensed under the Mozilla Public License (MPLv2)
+//
+/*global define */
+(function (root, factory) {
+    define([
+        "sizzle",
+        "../headless/lodash.noconflict",
+        "../headless/utils/core",
+        "urijs",
+        "../templates/audio.html",
+        "../headless/templates/field.html",
+        "../templates/file.html",
+        "../templates/form_captcha.html",
+        "../templates/form_checkbox.html",
+        "../templates/form_input.html",
+        "../templates/form_select.html",
+        "../templates/form_textarea.html",
+        "../templates/form_url.html",
+        "../templates/form_username.html",
+        "../templates/image.html",
+        "../templates/select_option.html",
+        "../templates/video.html"
+    ], factory);
+}(this, function (
+        sizzle,
+        _,
+        u,
+        URI,
+        tpl_audio,
+        tpl_field,
+        tpl_file,
+        tpl_form_captcha,
+        tpl_form_checkbox,
+        tpl_form_input,
+        tpl_form_select,
+        tpl_form_textarea,
+        tpl_form_url,
+        tpl_form_username,
+        tpl_image,
+        tpl_select_option,
+        tpl_video
+    ) {
+    "use strict";
+
+    const URL_REGEX = /\b(https?:\/\/|www\.|https?:\/\/www\.)[^\s<>]{2,200}\b\/?/g;
+
+    const logger = _.assign({
+        'debug': _.get(console, 'log') ? console.log.bind(console) : _.noop,
+        'error': _.get(console, 'log') ? console.log.bind(console) : _.noop,
+        'info': _.get(console, 'log') ? console.log.bind(console) : _.noop,
+        'warn': _.get(console, 'log') ? console.log.bind(console) : _.noop
+    }, console);
+
+    const XFORM_TYPE_MAP = {
+        'text-private': 'password',
+        'text-single': 'text',
+        'fixed': 'label',
+        'boolean': 'checkbox',
+        'hidden': 'hidden',
+        'jid-multi': 'textarea',
+        'list-single': 'dropdown',
+        'list-multi': 'dropdown'
+    };
+
+    function slideOutWrapup (el) {
+        /* Wrapup function for slideOut. */
+        el.removeAttribute('data-slider-marker');
+        el.classList.remove('collapsed');
+        el.style.overflow = "";
+        el.style.height = "";
+    }
+
+
+    const isImage = function (url) {
+        return new Promise((resolve, reject) => {
+            var img = new Image();
+            var timer = window.setTimeout(function () {
+                reject(new Error("Could not determine whether it's an image"));
+                img = null;
+            }, 3000);
+            img.onerror = img.onabort = function () {
+                clearTimeout(timer);
+                reject(new Error("Could not determine whether it's an image"));
+            };
+            img.onload = function () {
+                clearTimeout(timer);
+                resolve(img);
+            };
+            img.src = url;
+        });
+    };
+
+
+    u.isAudioURL = function (url) {
+        if (!(url instanceof URI)) {
+            url = new URI(url);
+        }
+        const filename = url.filename().toLowerCase();
+        if (!_.includes(["https", "http"], url.protocol().toLowerCase())) {
+            return false;
+        }
+        return filename.endsWith('.ogg') || filename.endsWith('.mp3') || filename.endsWith('.m4a');
+    }
+
+
+    u.isImageURL = function (url) {
+        if (!(url instanceof URI)) {
+            url = new URI(url);
+        }
+        const filename = url.filename().toLowerCase();
+        if (!_.includes(["https", "http"], url.protocol().toLowerCase())) {
+            return false;
+        }
+        return filename.endsWith('.jpg') || filename.endsWith('.jpeg') ||
+               filename.endsWith('.png') || filename.endsWith('.gif') ||
+               filename.endsWith('.bmp') || filename.endsWith('.tiff') ||
+               filename.endsWith('.svg');
+    };
+
+
+    u.isVideoURL = function (url) {
+        if (!(url instanceof URI)) {
+            url = new URI(url);
+        }
+        const filename = url.filename().toLowerCase();
+        if (!_.includes(["https", "http"], url.protocol().toLowerCase())) {
+            return false;
+        }
+        return filename.endsWith('.mp4') || filename.endsWith('.webm');
+    }
+
+
+    u.renderAudioURL = function (_converse, url) {
+        const uri = new URI(url);
+        if (u.isAudioURL(uri)) {
+            const { __ } = _converse;
+            return tpl_audio({
+                'url': url,
+                'label_download': __('Download audio file "%1$s"', decodeURI(uri.filename()))
+            })
+        }
+        return url;
+    };
+
+
+    u.renderFileURL = function (_converse, url) {
+        const uri = new URI(url);
+        if (u.isImageURL(uri) || u.isVideoURL(uri) || u.isAudioURL(uri)) {
+            return url;
+        }
+        const { __ } = _converse,
+              filename = uri.filename();
+        return tpl_file({
+            'url': url,
+            'label_download': __('Download file "%1$s"', decodeURI(filename))
+        })
+    };
+
+
+    u.renderImageURL = function (_converse, url) {
+        if (!_converse.show_images_inline) {
+            return u.addHyperlinks(url);
+        }
+        const uri = new URI(url);
+        if (u.isImageURL(uri)) {
+            const { __ } = _converse;
+            return tpl_image({
+                'url': url,
+                'label_download': __('Download image "%1$s"', decodeURI(uri.filename()))
+            })
+        }
+        return url;
+    };
+
+
+    u.renderImageURLs = function (_converse, el) {
+        /* Returns a Promise which resolves once all images have been loaded.
+         */
+        if (!_converse.show_images_inline) {
+            return Promise.resolve();
+        }
+        const { __ } = _converse;
+        const list = el.textContent.match(URL_REGEX) || [];
+        return Promise.all(
+            _.map(list, url =>
+                new Promise((resolve, reject) => {
+                    if (u.isImageURL(url)) {
+                        return isImage(url).then(img => {
+                            const i = new Image();
+                            i.src = img.src;
+                            i.addEventListener('load', resolve);
+                            // We also resolve for non-images, otherwise the
+                            // Promise.all resolves prematurely.
+                            i.addEventListener('error', resolve);
+
+                            const { __ } = _converse;
+                            _.each(sizzle(`a[href="${url}"]`, el), (a) => {
+                                a.outerHTML= tpl_image({
+                                    'url': url,
+                                    'label_download': __('Download')
+                                })
+                            });
+                        }).catch(resolve)
+                    } else {
+                        return resolve();
+                    }
+                })
+            )
+        )
+    };
+
+
+    u.renderMovieURL = function (_converse, url) {
+        const uri = new URI(url);
+        if (u.isVideoURL(uri)) {
+            const { __ } = _converse;
+            return tpl_video({
+                'url': url,
+                'label_download': __('Download video file "%1$s"', decodeURI(uri.filename()))
+            })
+        }
+        return url;
+    };
+
+
+    u.renderNewLines = function (text) {
+        return text.replace(/\n\n+/g, '<br/><br/>').replace(/\n/g, '<br/>');
+    };
+
+    u.calculateElementHeight = function (el) {
+        /* Return the height of the passed in DOM element,
+         * based on the heights of its children.
+         */
+        return _.reduce(
+            el.children,
+            (result, child) => result + child.offsetHeight, 0
+        );
+    }
+
+    u.getNextElement = function (el, selector='*') {
+        let next_el = el.nextElementSibling;
+        while (!_.isNull(next_el) && !sizzle.matchesSelector(next_el, selector)) {
+            next_el = next_el.nextElementSibling;
+        }
+        return next_el;
+    }
+
+    u.getPreviousElement = function (el, selector='*') {
+        let prev_el = el.previousSibling;
+        while (!_.isNull(prev_el) && !sizzle.matchesSelector(prev_el, selector)) {
+            prev_el = prev_el.previousSibling
+        }
+        return prev_el;
+    }
+
+    u.getFirstChildElement = function (el, selector='*') {
+        let first_el = el.firstElementChild;
+        while (!_.isNull(first_el) && !sizzle.matchesSelector(first_el, selector)) {
+            first_el = first_el.nextSibling
+        }
+        return first_el;
+    }
+
+    u.getLastChildElement = function (el, selector='*') {
+        let last_el = el.lastElementChild;
+        while (!_.isNull(last_el) && !sizzle.matchesSelector(last_el, selector)) {
+            last_el = last_el.previousSibling
+        }
+        return last_el;
+    }
+
+    u.hasClass = function (className, el) {
+        return _.includes(el.classList, className);
+    };
+
+    u.addClass = function (className, el) {
+        if (el instanceof Element) {
+            el.classList.add(className);
+        }
+    }
+
+    u.removeClass = function (className, el) {
+        if (el instanceof Element) {
+            el.classList.remove(className);
+        }
+        return el;
+    }
+
+    u.removeElement = function (el) {
+        if (!_.isNil(el) && !_.isNil(el.parentNode)) {
+            el.parentNode.removeChild(el);
+        }
+    }
+
+    u.showElement = _.flow(
+        _.partial(u.removeClass, 'collapsed'),
+        _.partial(u.removeClass, 'hidden')
+    )
+
+    u.hideElement = function (el) {
+        if (!_.isNil(el)) {
+            el.classList.add('hidden');
+        }
+        return el;
+    }
+
+    u.ancestor = function (el, selector) {
+        let parent = el;
+        while (!_.isNil(parent) && !sizzle.matchesSelector(parent, selector)) {
+            parent = parent.parentElement;
+        }
+        return parent;
+    }
+
+    u.nextUntil = function (el, selector, include_self=false) {
+        /* Return the element's siblings until one matches the selector. */
+        const matches = [];
+        let sibling_el = el.nextElementSibling;
+        while (!_.isNil(sibling_el) && !sibling_el.matches(selector)) {
+            matches.push(sibling_el);
+            sibling_el = sibling_el.nextElementSibling;
+        }
+        return matches;
+    }
+
+    u.unescapeHTML = function (string) {
+        /* Helper method that replace HTML-escaped symbols with equivalent characters
+         * (e.g. transform occurrences of '&amp;' to '&')
+         *
+         * Parameters:
+         *  (String) string: a String containing the HTML-escaped symbols.
+         */
+        var div = document.createElement('div');
+        div.innerHTML = string;
+        return div.innerText;
+    };
+
+    u.escapeHTML = function (string) {
+        return string
+            .replace(/&/g, "&amp;")
+            .replace(/</g, "&lt;")
+            .replace(/>/g, "&gt;")
+            .replace(/"/g, "&quot;");
+    };
+
+
+    u.addMentionsMarkup = function (text, references, chatbox) {
+        if (chatbox.get('message_type') !== 'groupchat') {
+            return text;
+        }
+        const nick = chatbox.get('nick');
+        references
+            .sort((a, b) => b.begin - a.begin)
+            .forEach(ref => {
+                const mention = text.slice(ref.begin, ref.end)
+                chatbox;
+                if (mention === nick) {
+                    text = text.slice(0, ref.begin) + `<span class="mention mention--self badge badge-info">${mention}</span>` + text.slice(ref.end);
+                } else {
+                    text = text.slice(0, ref.begin) + `<span class="mention">${mention}</span>` + text.slice(ref.end);
+                }
+            });
+        return text;
+    };
+
+
+    u.addHyperlinks = function (text) {
+        return URI.withinString(text, url => {
+            const uri = new URI(url);
+            url = uri.normalize()._string;
+            const pretty_url = uri._parts.urn ? url : uri.readable();
+            if (!uri._parts.protocol && !url.startsWith('http://') && !url.startsWith('https://')) {
+                url = 'http://' + url;
+            }
+            if (uri._parts.protocol === 'xmpp' && uri._parts.query === 'join') {
+                return `<a target="_blank" rel="noopener" class="open-chatroom" href="${url}">${u.escapeHTML(pretty_url)}</a>`;
+            }
+            return `<a target="_blank" rel="noopener" href="${url}">${u.escapeHTML(pretty_url)}</a>`;
+        }, {
+            'start': /\b(?:([a-z][a-z0-9.+-]*:\/\/)|xmpp:|mailto:|www\.)/gi
+        });
+    };
+
+
+    u.slideInAllElements = function (elements, duration=300) {
+        return Promise.all(
+            _.map(
+                elements,
+                _.partial(u.slideIn, _, duration)
+            ));
+    };
+
+    u.slideToggleElement = function (el, duration) {
+        if (_.includes(el.classList, 'collapsed') ||
+                _.includes(el.classList, 'hidden')) {
+            return u.slideOut(el, duration);
+        } else {
+            return u.slideIn(el, duration);
+        }
+    };
+
+
+    u.slideOut = function (el, duration=200) {
+        /* Shows/expands an element by sliding it out of itself
+         *
+         * Parameters:
+         *      (HTMLElement) el - The HTML string
+         *      (Number) duration - The duration amount in milliseconds
+         */
+        return new Promise((resolve, reject) => {
+            if (_.isNil(el)) {
+                const err = "Undefined or null element passed into slideOut"
+                logger.warn(err);
+                reject(new Error(err));
+                return;
+            }
+            const marker = el.getAttribute('data-slider-marker');
+            if (marker) {
+                el.removeAttribute('data-slider-marker');
+                window.cancelAnimationFrame(marker);
+            }
+            const end_height = u.calculateElementHeight(el);
+            if (window.converse_disable_effects) { // Effects are disabled (for tests)
+                el.style.height = end_height + 'px';
+                slideOutWrapup(el);
+                resolve();
+                return;
+            }
+            if (!u.hasClass('collapsed', el) && !u.hasClass('hidden', el)) {
+                resolve();
+                return;
+            }
+
+            const steps = duration/17; // We assume 17ms per animation which is ~60FPS
+            let height = 0;
+
+            function draw () {
+                height += end_height/steps;
+                if (height < end_height) {
+                    el.style.height = height + 'px';
+                    el.setAttribute(
+                        'data-slider-marker',
+                        window.requestAnimationFrame(draw)
+                    );
+                } else {
+                    // We recalculate the height to work around an apparent
+                    // browser bug where browsers don't know the correct
+                    // offsetHeight beforehand.
+                    el.removeAttribute('data-slider-marker');
+                    el.style.height = u.calculateElementHeight(el) + 'px';
+                    el.style.overflow = "";
+                    el.style.height = "";
+                    resolve();
+                }
+            }
+            el.style.height = '0';
+            el.style.overflow = 'hidden';
+            el.classList.remove('hidden');
+            el.classList.remove('collapsed');
+            el.setAttribute(
+                'data-slider-marker',
+                window.requestAnimationFrame(draw)
+            );
+        });
+    };
+
+    u.slideIn = function (el, duration=200) {
+        /* Hides/collapses an element by sliding it into itself. */
+        return new Promise((resolve, reject) => {
+            if (_.isNil(el)) {
+                const err = "Undefined or null element passed into slideIn";
+                logger.warn(err);
+                return reject(new Error(err));
+            } else if (_.includes(el.classList, 'collapsed')) {
+                return resolve(el);
+            } else if (window.converse_disable_effects) { // Effects are disabled (for tests)
+                el.classList.add('collapsed');
+                el.style.height = "";
+                return resolve(el);
+            }
+            const marker = el.getAttribute('data-slider-marker');
+            if (marker) {
+                el.removeAttribute('data-slider-marker');
+                window.cancelAnimationFrame(marker);
+            }
+            const original_height = el.offsetHeight,
+                 steps = duration/17; // We assume 17ms per animation which is ~60FPS
+            let height = original_height;
+
+            el.style.overflow = 'hidden';
+
+            function draw () {
+                height -= original_height/steps;
+                if (height > 0) {
+                    el.style.height = height + 'px';
+                    el.setAttribute(
+                        'data-slider-marker',
+                        window.requestAnimationFrame(draw)
+                    );
+                } else {
+                    el.removeAttribute('data-slider-marker');
+                    el.classList.add('collapsed');
+                    el.style.height = "";
+                    resolve(el);
+                }
+            }
+            el.setAttribute(
+                'data-slider-marker',
+                window.requestAnimationFrame(draw)
+            );
+        });
+    };
+
+    function afterAnimationEnds (el, callback) {
+        el.classList.remove('visible');
+        if (_.isFunction(callback)) {
+            callback();
+        }
+    }
+
+    u.fadeIn = function (el, callback) {
+        if (_.isNil(el)) {
+            logger.warn("Undefined or null element passed into fadeIn");
+        }
+        if (window.converse_disable_effects) {
+            el.classList.remove('hidden');
+            return afterAnimationEnds(el, callback);
+        }
+        if (_.includes(el.classList, 'hidden')) {
+            el.classList.add('visible');
+            el.classList.remove('hidden');
+            el.addEventListener("webkitAnimationEnd", _.partial(afterAnimationEnds, el, callback));
+            el.addEventListener("animationend", _.partial(afterAnimationEnds, el, callback));
+            el.addEventListener("oanimationend", _.partial(afterAnimationEnds, el, callback));
+        } else {
+            afterAnimationEnds(el, callback);
+        }
+    };
+
+
+    u.xForm2webForm = function (field, stanza, domain) {
+        /* Takes a field in XMPP XForm (XEP-004: Data Forms) format
+         * and turns it into an HTML field.
+         *
+         * Returns either text or a DOM element (which is not ideal, but fine
+         * for now).
+         *
+         *  Parameters:
+         *      (XMLElement) field - the field to convert
+         */
+        if (field.getAttribute('type')) {
+            if (field.getAttribute('type') === 'list-single' ||
+                field.getAttribute('type') === 'list-multi') {
+
+                const values = _.map(
+                    u.queryChildren(field, 'value'),
+                    _.partial(_.get, _, 'textContent')
+                );
+                const options = _.map(
+                    u.queryChildren(field, 'option'),
+                    function (option) {
+                        const value = _.get(option.querySelector('value'), 'textContent');
+                        return tpl_select_option({
+                            'value': value,
+                            'label': option.getAttribute('label'),
+                            'selected': _.includes(values, value),
+                            'required': !_.isNil(field.querySelector('required'))
+                        })
+                    }
+                );
+                return tpl_form_select({
+                    'id': u.getUniqueId(),
+                    'name': field.getAttribute('var'),
+                    'label': field.getAttribute('label'),
+                    'options': options.join(''),
+                    'multiple': (field.getAttribute('type') === 'list-multi'),
+                    'required': !_.isNil(field.querySelector('required'))
+                });
+            } else if (field.getAttribute('type') === 'fixed') {
+                const text = _.get(field.querySelector('value'), 'textContent');
+                return '<p class="form-help">'+text+'</p>';
+            } else if (field.getAttribute('type') === 'jid-multi') {
+                return tpl_form_textarea({
+                    'name': field.getAttribute('var'),
+                    'label': field.getAttribute('label') || '',
+                    'value': _.get(field.querySelector('value'), 'textContent'),
+                    'required': !_.isNil(field.querySelector('required'))
+                });
+            } else if (field.getAttribute('type') === 'boolean') {
+                return tpl_form_checkbox({
+                    'id': u.getUniqueId(),
+                    'name': field.getAttribute('var'),
+                    'label': field.getAttribute('label') || '',
+                    'checked': _.get(field.querySelector('value'), 'textContent') === "1" && 'checked="1"' || '',
+                    'required': !_.isNil(field.querySelector('required'))
+                });
+            } else if (field.getAttribute('var') === 'url') {
+                return tpl_form_url({
+                    'label': field.getAttribute('label') || '',
+                    'value': _.get(field.querySelector('value'), 'textContent')
+                });
+            } else if (field.getAttribute('var') === 'username') {
+                return tpl_form_username({
+                    'domain': ' @'+domain,
+                    'name': field.getAttribute('var'),
+                    'type': XFORM_TYPE_MAP[field.getAttribute('type')],
+                    'label': field.getAttribute('label') || '',
+                    'value': _.get(field.querySelector('value'), 'textContent'),
+                    'required': !_.isNil(field.querySelector('required'))
+                });
+            } else {
+                return tpl_form_input({
+                    'id': u.getUniqueId(),
+                    'label': field.getAttribute('label') || '',
+                    'name': field.getAttribute('var'),
+                    'placeholder': null,
+                    'required': !_.isNil(field.querySelector('required')),
+                    'type': XFORM_TYPE_MAP[field.getAttribute('type')],
+                    'value': _.get(field.querySelector('value'), 'textContent')
+                });
+            }
+        } else {
+            if (field.getAttribute('var') === 'ocr') { // Captcha
+                const uri = field.querySelector('uri');
+                const el = sizzle('data[cid="'+uri.textContent.replace(/^cid:/, '')+'"]', stanza)[0];
+                return tpl_form_captcha({
+                    'label': field.getAttribute('label'),
+                    'name': field.getAttribute('var'),
+                    'data': _.get(el, 'textContent'),
+                    'type': uri.getAttribute('type'),
+                    'required': !_.isNil(field.querySelector('required'))
+                });
+            }
+        }
+    }
+    return u;
+}));