Selaa lähdekoodia

Turn split.js into a component

JC Brand 8 kuukautta sitten
vanhempi
commit
10947865c8

+ 1 - 1
src/headless/types/shared/settings/constants.d.ts

@@ -72,7 +72,7 @@ export type ConfigurationSettings = {
     sid?: string;
     singleton?: boolean;
     strict_plugin_dependencies?: boolean;
-    view_mode?: ("overlayed" | "fullscreen" | "embedded");
+    view_mode?: ("fullscreen" | "embedded" | "overlayed");
     websocket_url?: string;
     whitelisted_plugins?: Array<string>;
 };

+ 8 - 8
src/headless/types/utils/index.d.ts

@@ -17,8 +17,6 @@ export function getRandomInt(max: any): number;
  */
 export function getUniqueId(suffix?: string): string;
 declare const _default: {
-    getLongestSubstring: typeof getLongestSubstring;
-    getOpenPromise: any;
     getRandomInt: typeof getRandomInt;
     getUniqueId: typeof getUniqueId;
     isEmptyMessage: typeof isEmptyMessage;
@@ -27,11 +25,9 @@ declare const _default: {
     prefixMentions: typeof prefixMentions;
     safeSave: typeof safeSave;
     shouldCreateMessage: typeof shouldCreateMessage;
-    toStanza: typeof toStanza;
     triggerEvent: typeof triggerEvent;
-    waitUntil: typeof waitUntil;
     isValidURL(text: string): boolean;
-    getURI(url: string | getOpenPromise): any;
+    getURI(url: string | promise.getOpenPromise): any;
     checkFileTypes(types: string[], url: string): boolean;
     filterQueryParamsFromURL(url: any): any;
     isURLWithImageExtension(url: any): boolean;
@@ -44,6 +40,9 @@ declare const _default: {
         media_urls?: MediaURLMetadata[];
     };
     getMediaURLs(arr: Array<MediaURLMetadata>, text: string, offset?: number): MediaURLData[];
+    firstCharToUpperCase(text: string): string;
+    getLongestSubstring(string: string, candidates: string[]): string;
+    isString(s: any): boolean;
     getDefaultStore(): "session" | "persistent";
     createStore(id: any, store: any): any;
     initStorage(model: any, id: any, type: any): void;
@@ -51,6 +50,7 @@ declare const _default: {
     isForbiddenError(stanza: Element): boolean;
     isServiceUnavailableError(stanza: Element): boolean;
     getAttributes(stanza: Element): object;
+    toStanza: typeof stanza.toStanza;
     isUniView(): boolean;
     isTestEnv(): boolean;
     getUnloadEvent(): "pagehide" | "beforeunload" | "unload";
@@ -58,6 +58,8 @@ declare const _default: {
     shouldClearCache(_converse: ConversePrivateGlobal): boolean;
     tearDown(_converse: ConversePrivateGlobal): Promise<any>;
     clearSession(_converse: ConversePrivateGlobal): any;
+    waitUntil(func: Function, max_wait?: number, check_delay?: number): Promise<any>;
+    getOpenPromise: any;
     merge(dst: any, src: any): void;
     isError(obj: any): boolean;
     isFunction(val: any): boolean;
@@ -93,7 +95,6 @@ export type CommonUtils = Record<string, Function>;
  * The utils object
  */
 export type PluginUtils = Record<"muc" | "mam", CommonUtils>;
-declare function getLongestSubstring(string: any, candidates: any): any;
 /**
  * Call the callback once all the events have been triggered
  * @param { Array } events: An array of objects, with keys `object` and
@@ -103,7 +104,6 @@ declare function getLongestSubstring(string: any, candidates: any): any;
  */
 declare function onMultipleEvents(events: any[], callback: Function): void;
 declare function shouldCreateMessage(attrs: any): any;
-import { toStanza } from 'strophe.js';
 /**
  * @param {Element} el
  * @param {string} name
@@ -112,7 +112,7 @@ import { toStanza } from 'strophe.js';
  * @param {boolean} [cancelable]
  */
 declare function triggerEvent(el: Element, name: string, type?: string, bubbles?: boolean, cancelable?: boolean): void;
-import { waitUntil } from './promise.js';
 import * as url from './url.js';
+import * as stanza from './stanza.js';
 import * as session from './session.js';
 //# sourceMappingURL=index.d.ts.map

+ 1 - 0
src/headless/types/utils/promise.d.ts

@@ -11,4 +11,5 @@
  * @license MIT
  */
 export function waitUntil(func: Function, max_wait?: number, check_delay?: number): Promise<any>;
+export { getOpenPromise };
 //# sourceMappingURL=promise.d.ts.map

+ 2 - 0
src/headless/types/utils/stanza.d.ts

@@ -19,4 +19,6 @@ export function isServiceUnavailableError(stanza: Element): boolean;
  * @returns {object}
  */
 export function getAttributes(stanza: Element): object;
+export { toStanza };
+import { toStanza } from 'strophe.js';
 //# sourceMappingURL=stanza.d.ts.map

+ 17 - 0
src/headless/types/utils/text.d.ts

@@ -0,0 +1,17 @@
+/**
+ * @param {string} text
+ * @returns {string}
+ */
+export function firstCharToUpperCase(text: string): string;
+/**
+ * @param {string} string
+ * @param {string[]} candidates
+ * @returns {string}
+ */
+export function getLongestSubstring(string: string, candidates: string[]): string;
+/**
+ * @param {any} s
+ * @returns {boolean}
+ */
+export function isString(s: any): boolean;
+//# sourceMappingURL=text.d.ts.map

+ 10 - 2
src/headless/utils/text.js

@@ -22,13 +22,13 @@ export function firstCharToUpperCase(text) {
  * @param {string[]} candidates
  * @returns {string}
  */
-export function getLongestSubstring (string, candidates) {
+export function getLongestSubstring(string, candidates) {
     /**
      * @param {string} accumulator
      * @param {string} current_value
      * @returns {string}
      */
-    function reducer (accumulator, current_value) {
+    function reducer(accumulator, current_value) {
         if (string.startsWith(current_value)) {
             if (current_value.length > accumulator.length) {
                 return current_value;
@@ -41,3 +41,11 @@ export function getLongestSubstring (string, candidates) {
     }
     return candidates.reduce(reducer, '');
 }
+
+/**
+ * @param {any} s
+ * @returns {boolean}
+ */
+export function isString(s) {
+    return typeof s === 'string' || s instanceof String;
+}

+ 0 - 14
src/plugins/muc-views/chatarea.js

@@ -1,6 +1,5 @@
 import { api, converse } from '@converse/headless';
 import { __ } from 'i18n';
-import Split from 'shared/split.js';
 import { CustomElement } from 'shared/components/element.js';
 import tplMUCChatarea from './templates/muc-chatarea.js';
 
@@ -35,19 +34,6 @@ export default class MUCChatArea extends CustomElement {
         return this.model ? tplMUCChatarea(this) : '';
     }
 
-    /**
-     * @param {Map<string, any>} changed
-     */
-    updated(changed) {
-        super.updated(changed);
-        if (!this.split) {
-            const sidebar_el = this.querySelector('converse-muc-sidebar');
-            if (sidebar_el) {
-                const chatarea_el = this.querySelector('.chat-area');
-                this.split = Split([chatarea_el, sidebar_el]);
-            }
-        }
-    }
 
     shouldShowSidebar () {
         return (

+ 1 - 1
src/plugins/muc-views/styles/muc.scss

@@ -20,7 +20,7 @@
             }
         }
 
-        .gutter {
+        converse-split-resize {
             background-color: var(--muc-color);
         }
 

+ 2 - 0
src/plugins/muc-views/templates/muc-chatarea.js

@@ -4,6 +4,7 @@ import '../bottom-panel.js';
 import '../sidebar.js';
 import 'shared/chat/chat-content.js';
 import 'shared/chat/help-messages.js';
+import 'shared/components/split-resize.js';
 
 const { CHATROOMS_TYPE } = constants;
 
@@ -43,6 +44,7 @@ export default (el) => {
             <converse-muc-bottom-panel jid="${el.jid}" class="bottom-panel"></converse-muc-bottom-panel>
         </div>
         ${el.model ? html`
+            <converse-split-resize></converse-split-resize>
             <converse-muc-sidebar
                 class="${el.shouldShowSidebar() ? sidebar_classes : 'col-xs-0 hidden' }"
                 jid=${el.jid}></converse-muc-sidebar>` : '' }`

+ 1 - 1
src/shared/components/image-picker.js

@@ -1,8 +1,8 @@
+import { html } from 'lit';
 import { Model } from '@converse/skeletor';
 import { CustomElement } from './element.js';
 import { __ } from 'i18n';
 import { api } from "@converse/headless";
-import { html } from 'lit';
 
 const i18n_profile_picture = __('Click to set a new picture');
 

+ 647 - 0
src/shared/components/split-resize.js

@@ -0,0 +1,647 @@
+/**
+ * @copyright Nathan Cahill and the Converse.js contributors
+ * @description Based on the split.js library from Nathan Cahill.
+ * @license MIT Licence
+ */
+import { html } from 'lit';
+import { api, u } from '@converse/headless';
+import { CustomElement } from './element.js';
+
+import './styles/split-resize.scss';
+
+const gutterStartDragging = '_a';
+const aGutterSize = '_b';
+const bGutterSize = '_c';
+const HORIZONTAL = 'horizontal';
+const NOOP = () => false;
+
+const global = typeof window !== 'undefined' ? window : null;
+
+export default class SplitResize extends CustomElement {
+    initialize() {
+        super.initialize();
+    }
+
+    constructor() {
+        super();
+        this.pair = null;
+    }
+
+    render() {
+        return html`<div class="gutter gutter-horizontal"></div>`;
+    }
+
+    disconnectedCallback() {
+        super.disconnectedCallback();
+        this.pair.gutter.removeEventListener('mousedown', this.pair[gutterStartDragging]);
+        this.pair.gutter.removeEventListener('touchstart', this.pair[gutterStartDragging]);
+    }
+
+    /**
+     * @param {Map<string, any>} changed
+     */
+    updated(changed) {
+        super.updated(changed);
+        if (!this.pair) {
+            this.setupSplit([
+                /** @type {HTMLElement} */ (this.previousElementSibling),
+                /** @type {HTMLElement} */ (this.nextElementSibling),
+            ]);
+        }
+    }
+
+    /**
+     * Helper function gets a property from the properties object, with a default fallback
+     */
+    getOption(options, propName, def) {
+        const value = options[propName];
+        if (value !== undefined) {
+            return value;
+        }
+        return def;
+    }
+
+    getElementStyle(dim, size, gutSize) {
+        const style = {};
+
+        if (!u.isString(size)) {
+            style[dim] = `calc(${size}% - ${gutSize}px)`;
+        } else {
+            style[dim] = size;
+        }
+
+        return style;
+    }
+
+    defaultGutterStyleFn(dim, gutSize) {
+        return { [dim]: `${gutSize}px` };
+    }
+
+    getGutterSize(gutterSize, isFirst, isLast, gutterAlign) {
+        if (isFirst) {
+            if (gutterAlign === 'end') {
+                return 0;
+            }
+            if (gutterAlign === 'center') {
+                return gutterSize / 2;
+            }
+        } else if (isLast) {
+            if (gutterAlign === 'start') {
+                return 0;
+            }
+            if (gutterAlign === 'center') {
+                return gutterSize / 2;
+            }
+        }
+
+        return gutterSize;
+    }
+
+    /**
+     * @param {HTMLElement} el
+     * @param {string} size
+     * @param {string} gutSize
+     */
+    setElementSize(el, size, gutSize) {
+        // Allows setting sizes via numbers (ideally), or if you must,
+        // by string, like '300px'. This is less than ideal, because it breaks
+        // the fluid layout that `calc(% - px)` provides. You're on your own if you do that,
+        // make sure you calculate the gutter size by hand.
+        const style = this.getElementStyle(this.dimension, size, gutSize);
+
+        Object.keys(style).forEach((prop) => {
+            // eslint-disable-next-line no-param-reassign
+            el.style[prop] = style[prop];
+        });
+    }
+
+    getSizes() {
+        return this.elements.map((element) => element.size);
+    }
+
+    /**
+     * Supports touch events, but not multitouch, so only the first
+     * finger `touches[0]` is counted.
+     * @param {MouseEvent} e
+     */
+    getMousePosition(e) {
+        if ('touches' in e) return e.touches[0][this.clientAxis];
+        return e[this.clientAxis];
+    }
+
+    /**
+     * Actually adjust the size of elements `a` and `b` to `offset` while dragging.
+     * calc is used to allow calc(percentage + gutterpx) on the whole split instance,
+     * which allows the viewport to be resized without additional logic.
+     * Element a's size is the same as offset. b's size is total size - a size.
+     * Both sizes are calculated from the initial parent percentage,
+     * then the gutter size is subtracted.
+     *
+     * @param {number} offset
+     */
+    adjust(offset) {
+        const a = this.elements[this.pair.a];
+        const b = this.elements[this.pair.b];
+        const percentage = a.size + b.size;
+
+        a.size = (offset / this.pair.size) * percentage;
+        b.size = percentage - (offset / this.pair.size) * percentage;
+
+        this.setElementSize(a.element, a.size, this.pair[aGutterSize]);
+        this.setElementSize(b.element, b.size, this.pair[bGutterSize]);
+    }
+
+    /**
+     * Handles the dragging logic for resizing elements.
+     *
+     * The logic is quite simple:
+     *
+     * 1. Ignore if the pair is not dragging.
+     * 2. Get the offset of the event.
+     * 3. Snap offset to min if within snappable range (within min + snapOffset).
+     * 4. Actually adjust each element in the pair to offset.
+     *
+     * ---------------------------------------------------------------------
+     * |    | <- a.minSize               ||              b.minSize -> |    |
+     * |    |  | <- this.snapOffset      ||     this.snapOffset -> |  |    |
+     * |    |  |                         ||                        |  |    |
+     * |    |  |                         ||                        |  |    |
+     * ---------------------------------------------------------------------
+     * | <- this.start                                        this.size -> |
+     *
+     * @param {MouseEvent} e
+     * @param {object} options
+     */
+    drag(e, options) {
+        let offset;
+        const a = this.elements[this.pair.a];
+        const b = this.elements[this.pair.b];
+
+        if (!this.pair.dragging) return;
+
+        // Get the offset of the event from the first side of the
+        // pair `pair.start`. Then offset by the initial position of the
+        // mouse compared to the gutter size.
+        offset = this.getMousePosition(e) - this.pair.start + (this.pair[aGutterSize] - this.pair.dragOffset);
+
+        if (this.dragInterval > 1) {
+            offset = Math.round(offset / this.dragInterval) * this.dragInterval;
+        }
+
+        // If within snapOffset of min or max, set offset to min or max.
+        // snapOffset buffers a.minSize and b.minSize, so logic is opposite for both.
+        // Include the appropriate gutter sizes to prevent overflows.
+        if (offset <= a.minSize + a.snapOffset + this.pair[aGutterSize]) {
+            offset = a.minSize + this.pair[aGutterSize];
+        } else if (offset >= this.pair.size - (b.minSize + b.snapOffset + this.pair[bGutterSize])) {
+            offset = this.pair.size - (b.minSize + this.pair[bGutterSize]);
+        }
+
+        if (offset >= a.maxSize - a.snapOffset + this.pair[aGutterSize]) {
+            offset = a.maxSize + this.pair[aGutterSize];
+        } else if (offset <= this.pair.size - (b.maxSize - b.snapOffset + this.pair[bGutterSize])) {
+            offset = this.pair.size - (b.maxSize + this.pair[bGutterSize]);
+        }
+
+        // Actually adjust the size.
+        this.adjust(offset);
+
+        // Call the drag callback continously. Don't do anything too intensive
+        // in pair callback.
+        this.getOption(options, 'onDrag', NOOP)(this.getSizes());
+    }
+
+    /**
+     * Cache some important sizes when drag starts, so we don't have to do that
+     * continuously:
+     *
+     * `size`: The total size of the pair. First + second + first gutter + second gutter.
+     * `start`: The leading side of the first element.
+     *
+     * ------------------------------------------------
+     * |      aGutterSize -> |||                      |
+     * |                     |||                      |
+     * |                     |||                      |
+     * |                     ||| <- bGutterSize       |
+     * ------------------------------------------------
+     * | <- start                             size -> |
+     *
+     * @param {ResizablePair} pair
+     */
+    calculateSizes(pair) {
+        // Figure out the parent size minus padding.
+        const a = this.elements[pair.a].element;
+        const b = this.elements[pair.b].element;
+
+        const aBounds = a.getBoundingClientRect();
+        const bBounds = b.getBoundingClientRect();
+
+        pair.size = aBounds[this.dimension] + bBounds[this.dimension] + pair[aGutterSize] + pair[bGutterSize];
+        pair.start = aBounds[this.position];
+        pair.end = aBounds[this.positionEnd];
+    }
+
+    /**
+     * @param {HTMLElement} el
+     */
+    innerSize(el) {
+        // Return nothing if getComputedStyle is not supported (< IE9)
+        // Or if parent el has no layout yet
+        if (!getComputedStyle) return null;
+
+        const computedStyle = getComputedStyle(el);
+        if (!computedStyle) return null;
+
+        let size = el[this.clientSize];
+        if (size === 0) return null;
+
+        if (this.direction === HORIZONTAL) {
+            size -= parseFloat(computedStyle.paddingLeft) + parseFloat(computedStyle.paddingRight);
+        } else {
+            size -= parseFloat(computedStyle.paddingTop) + parseFloat(computedStyle.paddingBottom);
+        }
+        return size;
+    }
+
+    /**
+     * When specifying percentage sizes that are less than the computed
+     * size of the element minus the gutter, the lesser percentages must be increased
+     * (and decreased from the other elements) to make space for the pixels
+     * subtracted by the gutters.
+     */
+    trimToMin(sizesToTrim) {
+        // Try to get inner size of parent element.
+        // If it's no supported, return original sizes.
+        const parentSize = this.innerSize(this.parent);
+        if (parentSize === null) {
+            return sizesToTrim;
+        }
+
+        if (this.minSizes.reduce((a, b) => a + b, 0) > parentSize) {
+            return sizesToTrim;
+        }
+
+        // Keep track of the excess pixels, the amount of pixels over the desired percentage
+        // Also keep track of the elements with pixels to spare, to decrease after if needed
+        let excessPixels = 0;
+        const toSpare = [];
+
+        const pixelSizes = sizesToTrim.map((size, i) => {
+            // Convert requested percentages to pixel sizes
+            const pixelSize = (parentSize * size) / 100;
+            const elementGutterSize = this.getGutterSize(
+                this.gutterSize,
+                i === 0,
+                i === sizesToTrim.length - 1,
+                this.gutterAlign
+            );
+            const elementMinSize = this.minSizes[i] + elementGutterSize;
+
+            // If element is too smal, increase excess pixels by the difference
+            // and mark that it has no pixels to spare
+            if (pixelSize < elementMinSize) {
+                excessPixels += elementMinSize - pixelSize;
+                toSpare.push(0);
+                return elementMinSize;
+            }
+
+            // Otherwise, mark the pixels it has to spare and return it's original size
+            toSpare.push(pixelSize - elementMinSize);
+            return pixelSize;
+        });
+
+        // If nothing was adjusted, return the original sizes
+        if (excessPixels === 0) {
+            return sizesToTrim;
+        }
+
+        return pixelSizes.map((pixelSize, i) => {
+            let newPixelSize = pixelSize;
+
+            // While there's still pixels to take, and there's enough pixels to spare,
+            // take as many as possible up to the total excess pixels
+            if (excessPixels > 0 && toSpare[i] - excessPixels > 0) {
+                const takenPixels = Math.min(excessPixels, toSpare[i] - excessPixels);
+
+                // Subtract the amount taken for the next iteration
+                excessPixels -= takenPixels;
+                newPixelSize = pixelSize - takenPixels;
+            }
+
+            // Return the pixel size adjusted as a percentage
+            return (newPixelSize / parentSize) * 100;
+        });
+    }
+
+    /**
+     * stopDragging is very similar to startDragging in reverse.
+     * @param {Object} options
+     */
+    stopDragging(options) {
+        const a = this.elements[this.pair.a].element;
+        const b = this.elements[this.pair.b].element;
+
+        if (this.pair.dragging) {
+            this.getOption(options, 'onDragEnd', NOOP)(this.getSizes());
+        }
+
+        this.pair.dragging = false;
+
+        // Remove the stored event listeners. This is why we store them.
+        global.removeEventListener('mouseup', this.pair.stop);
+        global.removeEventListener('touchend', this.pair.stop);
+        global.removeEventListener('touchcancel', this.pair.stop);
+        global.removeEventListener('mousemove', this.pair.move);
+        global.removeEventListener('touchmove', this.pair.move);
+
+        // Clear bound function references
+        this.pair.stop = null;
+        this.pair.move = null;
+
+        a.removeEventListener('selectstart', NOOP);
+        a.removeEventListener('dragstart', NOOP);
+        b.removeEventListener('selectstart', NOOP);
+        b.removeEventListener('dragstart', NOOP);
+
+        a.style.userSelect = '';
+        a.style.pointerEvents = '';
+
+        b.style.userSelect = '';
+        b.style.pointerEvents = '';
+
+        this.pair.gutter.style.cursor = '';
+        this.pair.parent.style.cursor = '';
+        document.body.style.cursor = '';
+    }
+
+    /**
+     * startDragging calls `calculateSizes` to store the initial size in the pair object.
+     * It also adds event listeners for mouse/touch events,
+     * and prevents selection while dragging to avoid selecting text.
+     * @param {MouseEvent} e
+     * @param {Object} options
+     */
+    startDragging(e, options) {
+        // Right-clicking can't start dragging.
+        if ('button' in e && e.button !== 0) {
+            return;
+        }
+
+        // Alias frequently used variables to save space. 200 bytes.
+        const a = this.elements[this.pair.a].element;
+        const b = this.elements[this.pair.b].element;
+
+        // Call the onDragStart callback.
+        if (!this.pair.dragging) {
+            this.getOption(options, 'onDragStart', NOOP)(this.getSizes());
+        }
+
+        // Don't actually drag the element. We emulate this in the drag function.
+        e.preventDefault();
+
+        // Set the dragging property of the this.pair object.
+        this.pair.dragging = true;
+
+        // Create two event listeners bound to the same this.pair object and store
+        // them in the this.pair object.
+        this.pair.stop = () => this.stopDragging(options);
+        this.pair.move = /** @param {MouseEvent} e */ (e) => this.drag(e, options);
+
+        // All the binding. `window` gets the stop events in case we drag out of the elements.
+        global.addEventListener('mouseup', this.pair.stop);
+        global.addEventListener('touchend', this.pair.stop);
+        global.addEventListener('touchcancel', this.pair.stop);
+        global.addEventListener('mousemove', this.pair.move);
+        global.addEventListener('touchmove', this.pair.move);
+
+        // Disable selection. Disable!
+        a.addEventListener('selectstart', NOOP);
+        a.addEventListener('dragstart', NOOP);
+        b.addEventListener('selectstart', NOOP);
+        b.addEventListener('dragstart', NOOP);
+
+        a.style.userSelect = 'none';
+        a.style.pointerEvents = 'none';
+
+        b.style.userSelect = 'none';
+        b.style.pointerEvents = 'none';
+
+        // Set the cursor at multiple levels
+        this.pair.gutter.style.cursor = this.cursor;
+        this.pair.parent.style.cursor = this.cursor;
+        document.body.style.cursor = this.cursor;
+
+        // Cache the initial sizes of the this.pair.
+        this.calculateSizes(this.pair);
+
+        // Determine the position of the mouse compared to the gutter
+        this.pair.dragOffset = this.getMousePosition(e) - this.pair.end;
+    }
+
+    /**
+     * @param {Object} element
+     */
+    adjustToMin(element) {
+        this.calculateSizes(this.pair);
+        this.adjust(this.pair.size - element.minSize - this.pair[bGutterSize]);
+    }
+
+    /**
+     * @param {Array<number>} newSizes
+     */
+    setSizes(newSizes) {
+        const trimmed = this.trimToMin(newSizes);
+        trimmed.forEach((newSize, i) => {
+            if (i > 0) {
+                const a = this.elements[this.pair.a];
+                const b = this.elements[this.pair.b];
+
+                a.size = trimmed[i - 1];
+                b.size = newSize;
+
+                this.setElementSize(a.element, a.size, this.pair[aGutterSize]);
+                this.setElementSize(b.element, b.size, this.pair[bGutterSize]);
+            }
+        });
+    }
+
+    /**
+     * The main function to initialize a split.
+     *
+     * Each pair of elements, resizable relative to one another, is handled independently.
+     * Dragging the gutter between two elements only changes the dimensions of elements in that pair.
+     *
+     * A pair object is shaped like this:
+     *
+     * @typedef {Object} ResizablePair
+     * @property {(0|1)} a
+     * @property {(0|1)} b
+     * @property {('horizontal'|'vertical')} direction
+     * @property {boolean} dragging
+     * @property {number} aMin
+     * @property {number} bMin
+     * @property {number} dragOffset
+     * @property {number} size
+     * @property {number} start
+     * @property {number} end
+     * @property {HTMLElement} gutter
+     * @property {HTMLElement} parent
+     * @property {(this: Window, ev: Event) => any} stop
+     * @property {(this: Window, ev: Event) => any} move
+     *
+     * The basic sequence:
+     *
+     * 1. Set defaults to something sane. `options` doesn't have to be passed at all.
+     * 2. Initialize a bunch of strings based on the direction we're splitting.
+     *    A lot of the behavior in the rest of the library is parameterized down to
+     *    rely on CSS strings and classes.
+     * 3. Define the dragging helper functions, and a few helpers to go with them.
+     * 4. Loop through the elements while pairing them off. Every pair gets an
+     *    `pair` object and a gutter.
+     * 5. Actually size the pair elements, insert gutters and attach event listeners.
+     *
+     * @param {HTMLElement[]} els
+     */
+    setupSplit(els, options = {}) {
+        // Allow HTMLCollection to be used as an argument
+        els = Array.from(els);
+
+        // All DOM elements in the split should have a common parent. We can grab
+        // the first elements parent and hope users read the docs because the
+        // behavior will be whacky otherwise.
+        const firstElement = els[0];
+        this.parent = firstElement.parentElement;
+
+        // Standardize minSize and maxSize to an array if it isn't already.
+        // This allows minSize and maxSize to be passed as a number.
+        const minSize = this.getOption(options, 'minSize', 100);
+        this.minSizes = Array.isArray(minSize) ? minSize : els.map(() => minSize);
+
+        // Get other options
+        const expandToMin = this.getOption(options, 'expandToMin', false);
+        this.gutterSize = this.getOption(options, 'gutterSize', 5);
+        this.gutterAlign = this.getOption(options, 'gutterAlign', 'center');
+        this.dragInterval = this.getOption(options, 'dragInterval', 1);
+        this.direction = this.getOption(options, 'direction', HORIZONTAL);
+        this.cursor = this.getOption(options, 'cursor', this.direction === HORIZONTAL ? 'col-resize' : 'row-resize');
+
+        // 2. Initialize a bunch of strings based on the direction we're splitting.
+        // A lot of the behavior in the rest of the library is paramatized down to
+        // rely on CSS strings and classes.
+        if (this.direction === HORIZONTAL) {
+            this.dimension = 'width';
+            this.clientAxis = 'clientX';
+            this.position = 'left';
+            this.positionEnd = 'right';
+            this.clientSize = 'clientWidth';
+        } else if (this.direction === 'vertical') {
+            this.dimension = 'height';
+            this.clientAxis = 'clientY';
+            this.position = 'top';
+            this.positionEnd = 'bottom';
+            this.clientSize = 'clientHeight';
+        }
+
+        // Create pair and element objects. Each pair has an index reference to
+        // elements `a` and `b` of the pair (first and second elements).
+        // Loop through the elements while pairing them off. Every pair gets a
+        // `pair` object and a gutter.
+        //
+        // Basic logic:
+        //
+        // - Starting with the second element `i > 0`, create `pair` objects with
+        //   `a = i - 1` and `b = i`
+        // - Set gutter sizes based on the _pair_ being first/last. The first and last
+        //   pair have gutterSize / 2, since they only have one half gutter, and not two.
+        // - Create gutter elements and add event listeners.
+        // - Set the size of the elements, minus the gutter sizes.
+        //
+        // -----------------------------------------------------------------------
+        // |     i=0     |         i=1         |        i=2       |      i=3     |
+        // |             |                     |                  |              |
+        // |           pair 0                pair 1             pair 2           |
+        // |             |                     |                  |              |
+        // -----------------------------------------------------------------------
+        this.elements = els.map((el, i) => this.createElement(els, el, i, options));
+
+        this.elements.forEach((element) => {
+            const computedSize = element.element.getBoundingClientRect()[this.dimension];
+            if (computedSize < element.minSize) {
+                if (expandToMin) {
+                    this.adjustToMin(element);
+                } else {
+                    element.minSize = computedSize;
+                }
+            }
+        });
+
+        this.createPair(options);
+    }
+
+    /**
+     * @param {HTMLElement[]} els
+     * @param {HTMLElement} el
+     * @param {number} i
+     * @param {object} options
+     */
+    createElement(els, el, i, options) {
+        // Set default options.sizes to equal percentages of the parent element.
+        let sizes = this.getOption(options, 'sizes') || els.map(() => 100 / els.length);
+        // adjust sizes to ensure percentage is within min size and gutter.
+        sizes = this.trimToMin(sizes);
+
+        const maxSize = this.getOption(options, 'maxSize', Infinity);
+        const maxSizes = Array.isArray(maxSize) ? maxSize : els.map(() => maxSize);
+
+        const snapOffset = this.getOption(options, 'snapOffset', 30);
+        const snapOffsets = Array.isArray(snapOffset) ? snapOffset : els.map(() => snapOffset);
+
+        return {
+            element: el,
+            size: sizes[i],
+            minSize: this.minSizes[i],
+            maxSize: maxSizes[i],
+            snapOffset: snapOffsets[i],
+            i,
+        };
+    }
+
+    /**
+     * @param {object} options
+     */
+    createPair(options) {
+        const parentStyle = getComputedStyle ? getComputedStyle(this.parent) : null;
+        const parentFlexDirection = parentStyle ? parentStyle.flexDirection : null;
+
+        // Create the pair object with its metadata.
+        this.pair = /** @type {ResizablePair} */ ({
+            a: 0,
+            b: 1,
+            dragging: false,
+            direction: this.direction,
+            parent: this.parent,
+        });
+
+        this.pair[aGutterSize] = this.getGutterSize(this.gutterSize, true, false, this.gutterAlign);
+        this.pair[bGutterSize] = this.getGutterSize(this.gutterSize, false, true, this.gutterAlign);
+
+        // if the parent has a reverse flex-direction, switch the pair elements.
+        if (parentFlexDirection === 'row-reverse' || parentFlexDirection === 'column-reverse') {
+            const temp = this.pair.a;
+            this.pair.a = this.pair.b;
+            this.pair.b = temp;
+        }
+
+        const gutterElement = /** @type {HTMLElement} */ (this.firstElementChild);
+        // Save bound event listener for removal later
+        this.pair[gutterStartDragging] = (e) => this.startDragging(e, options);
+
+        // Attach bound event listener
+        this.addEventListener('mousedown', this.pair[gutterStartDragging]);
+        this.addEventListener('touchstart', this.pair[gutterStartDragging]);
+
+        this.pair.gutter = gutterElement;
+    }
+}
+
+api.elements.define('converse-split-resize', SplitResize);

+ 9 - 0
src/shared/components/styles/split-resize.scss

@@ -0,0 +1,9 @@
+.conversejs {
+    converse-split-resize {
+        background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg==');
+        background-position: 50%;
+        background-repeat: no-repeat;
+        cursor: col-resize;
+        width: 0.25em !important;
+    }
+}

+ 0 - 690
src/shared/split.js

@@ -1,690 +0,0 @@
-/**
- * @copyright Nathan Cahill and the Converse.js contributors
- * @description Based on the split.js library from Nathan Cahill.
- * @license MIT Licence
- */
-const global = typeof window !== 'undefined' ? window : null;
-const ssr = global === null;
-const document = !ssr ? global.document : undefined;
-
-// Save a couple long function names that are used frequently.
-// This optimization saves around 400 bytes.
-const addEventListener = 'addEventListener';
-const removeEventListener = 'removeEventListener';
-const getBoundingClientRect = 'getBoundingClientRect';
-const gutterStartDragging = '_a';
-const aGutterSize = '_b';
-const bGutterSize = '_c';
-const HORIZONTAL = 'horizontal';
-const NOOP = () => false;
-
-// Helper function determines which prefixes of CSS calc we need.
-// We only need to do this once on startup, when this anonymous function is called.
-//
-// Tests -webkit, -moz and -o prefixes. Modified from StackOverflow:
-// http://stackoverflow.com/questions/16625140/js-feature-detection-to-detect-the-usage-of-webkit-calc-over-calc/16625167#16625167
-const calc = ssr
-    ? 'calc'
-    : `${['', '-webkit-', '-moz-', '-o-']
-          .filter((prefix) => {
-              const el = document.createElement('div');
-              el.style.cssText = `width:${prefix}calc(9px)`;
-
-              return !!el.style.length;
-          })
-          .shift()}calc`;
-
-// Helper function checks if its argument is a string-like type
-const isString = (v) => typeof v === 'string' || v instanceof String;
-
-// Helper function gets a property from the properties object, with a default fallback
-const getOption = (options, propName, def) => {
-    const value = options[propName];
-    if (value !== undefined) {
-        return value;
-    }
-    return def;
-};
-
-const getGutterSize = (gutterSize, isFirst, isLast, gutterAlign) => {
-    if (isFirst) {
-        if (gutterAlign === 'end') {
-            return 0;
-        }
-        if (gutterAlign === 'center') {
-            return gutterSize / 2;
-        }
-    } else if (isLast) {
-        if (gutterAlign === 'start') {
-            return 0;
-        }
-        if (gutterAlign === 'center') {
-            return gutterSize / 2;
-        }
-    }
-
-    return gutterSize;
-};
-
-// Default options
-const defaultGutterFn = (i, gutterDirection) => {
-    const gut = document.createElement('div');
-    gut.className = `gutter gutter-${gutterDirection}`;
-    return gut;
-};
-
-const defaultElementStyleFn = (dim, size, gutSize) => {
-    const style = {};
-
-    if (!isString(size)) {
-        style[dim] = `${calc}(${size}% - ${gutSize}px)`;
-    } else {
-        style[dim] = size;
-    }
-
-    return style;
-};
-
-const defaultGutterStyleFn = (dim, gutSize) => ({ [dim]: `${gutSize}px` });
-
-/**
- * The main function to initialize a split. Split.js thinks about each pair
- * of elements as an independent pair. Dragging the gutter between two elements
- * only changes the dimensions of elements in that pair. This is key to understanding
- * how the following functions operate, since each function is bound to a pair.
- *
- * A pair object is shaped like this:
- *
- * {
- *     a: DOM element,
- *     b: DOM element,
- *     aMin: Number,
- *     bMin: Number,
- *     dragging: Boolean,
- *     parent: DOM element,
- *     direction: 'horizontal' | 'vertical'
- * }
- *
- * The basic sequence:
- *
- * 1. Set defaults to something sane. `options` doesn't have to be passed at all.
- * 2. Initialize a bunch of strings based on the direction we're splitting.
- *    A lot of the behavior in the rest of the library is parameterized down to
- *    rely on CSS strings and classes.
- * 3. Define the dragging helper functions, and a few helpers to go with them.
- * 4. Loop through the elements while pairing them off. Every pair gets an
- *    `pair` object and a gutter.
- * 5. Actually size the pair elements, insert gutters and attach event listeners.
- */
-const Split = (els, options = {}) => {
-    if (ssr) return {};
-
-    let dimension;
-    let clientAxis;
-    let position;
-    let positionEnd;
-    let clientSize;
-    let elements;
-
-    // Allow HTMLCollection to be used as an argument when supported
-    if (Array.from) {
-        els = Array.from(els);
-    }
-
-    // All DOM elements in the split should have a common parent. We can grab
-    // the first elements parent and hope users read the docs because the
-    // behavior will be whacky otherwise.
-    const firstElement = els[0];
-    const parent = firstElement.parentNode;
-    const parentStyle = getComputedStyle ? getComputedStyle(parent) : null;
-    const parentFlexDirection = parentStyle ? parentStyle.flexDirection : null;
-
-    // Set default options.sizes to equal percentages of the parent element.
-    let sizes = getOption(options, 'sizes') || els.map(() => 100 / els.length);
-
-    // Standardize minSize and maxSize to an array if it isn't already.
-    // This allows minSize and maxSize to be passed as a number.
-    const minSize = getOption(options, 'minSize', 100);
-    const minSizes = Array.isArray(minSize) ? minSize : els.map(() => minSize);
-    const maxSize = getOption(options, 'maxSize', Infinity);
-    const maxSizes = Array.isArray(maxSize) ? maxSize : els.map(() => maxSize);
-
-    // Get other options
-    const expandToMin = getOption(options, 'expandToMin', false);
-    const gutterSize = getOption(options, 'gutterSize', 5);
-    const gutterAlign = getOption(options, 'gutterAlign', 'center');
-    const snapOffset = getOption(options, 'snapOffset', 30);
-    const snapOffsets = Array.isArray(snapOffset) ? snapOffset : els.map(() => snapOffset);
-    const dragInterval = getOption(options, 'dragInterval', 1);
-    const direction = getOption(options, 'direction', HORIZONTAL);
-    const cursor = getOption(options, 'cursor', direction === HORIZONTAL ? 'col-resize' : 'row-resize');
-    const gutter = getOption(options, 'gutter', defaultGutterFn);
-    const elementStyle = getOption(options, 'elementStyle', defaultElementStyleFn);
-    const gutterStyle = getOption(options, 'gutterStyle', defaultGutterStyleFn);
-
-    // 2. Initialize a bunch of strings based on the direction we're splitting.
-    // A lot of the behavior in the rest of the library is paramatized down to
-    // rely on CSS strings and classes.
-    if (direction === HORIZONTAL) {
-        dimension = 'width';
-        clientAxis = 'clientX';
-        position = 'left';
-        positionEnd = 'right';
-        clientSize = 'clientWidth';
-    } else if (direction === 'vertical') {
-        dimension = 'height';
-        clientAxis = 'clientY';
-        position = 'top';
-        positionEnd = 'bottom';
-        clientSize = 'clientHeight';
-    }
-
-    // 3. Define the dragging helper functions, and a few helpers to go with them.
-    // Each helper is bound to a pair object that contains its metadata. This
-    // also makes it easy to store references to listeners that that will be
-    // added and removed.
-    //
-    // Even though there are no other functions contained in them, aliasing
-    // this to self saves 50 bytes or so since it's used so frequently.
-    //
-    // The pair object saves metadata like dragging state, position and
-    // event listener references.
-
-    function setElementSize(el, size, gutSize, i) {
-        // Split.js allows setting sizes via numbers (ideally), or if you must,
-        // by string, like '300px'. This is less than ideal, because it breaks
-        // the fluid layout that `calc(% - px)` provides. You're on your own if you do that,
-        // make sure you calculate the gutter size by hand.
-        const style = elementStyle(dimension, size, gutSize, i);
-
-        Object.keys(style).forEach((prop) => {
-            // eslint-disable-next-line no-param-reassign
-            el.style[prop] = style[prop];
-        });
-    }
-
-    function setGutterSize(gutterElement, gutSize, i) {
-        const style = gutterStyle(dimension, gutSize, i);
-
-        Object.keys(style).forEach((prop) => {
-            // eslint-disable-next-line no-param-reassign
-            gutterElement.style[prop] = style[prop];
-        });
-    }
-
-    function getSizes() {
-        return elements.map((element) => element.size);
-    }
-
-    // Supports touch events, but not multitouch, so only the first
-    // finger `touches[0]` is counted.
-    function getMousePosition(e) {
-        if ('touches' in e) return e.touches[0][clientAxis];
-        return e[clientAxis];
-    }
-
-    /**
-     * Actually adjust the size of elements `a` and `b` to `offset` while dragging.
-     * calc is used to allow calc(percentage + gutterpx) on the whole split instance,
-     * which allows the viewport to be resized without additional logic.
-     * Element a's size is the same as offset. b's size is total size - a size.
-     * Both sizes are calculated from the initial parent percentage,
-     * then the gutter size is subtracted.
-     */
-    function adjust(offset) {
-        const a = elements[this.a];
-        const b = elements[this.b];
-        const percentage = a.size + b.size;
-
-        a.size = (offset / this.size) * percentage;
-        b.size = percentage - (offset / this.size) * percentage;
-
-        setElementSize(a.element, a.size, this[aGutterSize], a.i);
-        setElementSize(b.element, b.size, this[bGutterSize], b.i);
-    }
-
-    /**
-     * Handles the dragging logic for resizing elements.
-     *
-     * The logic is quite simple:
-     *
-     * 1. Ignore if the pair is not dragging.
-     * 2. Get the offset of the event.
-     * 3. Snap offset to min if within snappable range (within min + snapOffset).
-     * 4. Actually adjust each element in the pair to offset.
-     *
-     * ---------------------------------------------------------------------
-     * |    | <- a.minSize               ||              b.minSize -> |    |
-     * |    |  | <- this.snapOffset      ||     this.snapOffset -> |  |    |
-     * |    |  |                         ||                        |  |    |
-     * |    |  |                         ||                        |  |    |
-     * ---------------------------------------------------------------------
-     * | <- this.start                                        this.size -> |
-     */
-    function drag(e) {
-        let offset;
-        const a = elements[this.a];
-        const b = elements[this.b];
-
-        if (!this.dragging) return;
-
-        // Get the offset of the event from the first side of the
-        // pair `this.start`. Then offset by the initial position of the
-        // mouse compared to the gutter size.
-        offset = getMousePosition(e) - this.start + (this[aGutterSize] - this.dragOffset);
-
-        if (dragInterval > 1) {
-            offset = Math.round(offset / dragInterval) * dragInterval;
-        }
-
-        // If within snapOffset of min or max, set offset to min or max.
-        // snapOffset buffers a.minSize and b.minSize, so logic is opposite for both.
-        // Include the appropriate gutter sizes to prevent overflows.
-        if (offset <= a.minSize + a.snapOffset + this[aGutterSize]) {
-            offset = a.minSize + this[aGutterSize];
-        } else if (offset >= this.size - (b.minSize + b.snapOffset + this[bGutterSize])) {
-            offset = this.size - (b.minSize + this[bGutterSize]);
-        }
-
-        if (offset >= a.maxSize - a.snapOffset + this[aGutterSize]) {
-            offset = a.maxSize + this[aGutterSize];
-        } else if (offset <= this.size - (b.maxSize - b.snapOffset + this[bGutterSize])) {
-            offset = this.size - (b.maxSize + this[bGutterSize]);
-        }
-
-        // Actually adjust the size.
-        adjust.call(this, offset);
-
-        // Call the drag callback continously. Don't do anything too intensive
-        // in this callback.
-        getOption(options, 'onDrag', NOOP)(getSizes());
-    }
-
-    /**
-     * Cache some important sizes when drag starts, so we don't have to do that
-     * continuously:
-     *
-     * `size`: The total size of the pair. First + second + first gutter + second gutter.
-     * `start`: The leading side of the first element.
-     *
-     * ------------------------------------------------
-     * |      aGutterSize -> |||                      |
-     * |                     |||                      |
-     * |                     |||                      |
-     * |                     ||| <- bGutterSize       |
-     * ------------------------------------------------
-     * | <- start                             size -> |
-     */
-    function calculateSizes() {
-        // Figure out the parent size minus padding.
-        const a = elements[this.a].element;
-        const b = elements[this.b].element;
-
-        const aBounds = a[getBoundingClientRect]();
-        const bBounds = b[getBoundingClientRect]();
-
-        this.size = aBounds[dimension] + bBounds[dimension] + this[aGutterSize] + this[bGutterSize];
-        this.start = aBounds[position];
-        this.end = aBounds[positionEnd];
-    }
-
-    function innerSize(element) {
-        // Return nothing if getComputedStyle is not supported (< IE9)
-        // Or if parent element has no layout yet
-        if (!getComputedStyle) return null;
-
-        const computedStyle = getComputedStyle(element);
-
-        if (!computedStyle) return null;
-
-        let size = element[clientSize];
-
-        if (size === 0) return null;
-
-        if (direction === HORIZONTAL) {
-            size -= parseFloat(computedStyle.paddingLeft) + parseFloat(computedStyle.paddingRight);
-        } else {
-            size -= parseFloat(computedStyle.paddingTop) + parseFloat(computedStyle.paddingBottom);
-        }
-
-        return size;
-    }
-
-    /**
-     * When specifying percentage sizes that are less than the computed
-     * size of the element minus the gutter, the lesser percentages must be increased
-     * (and decreased from the other elements) to make space for the pixels
-     * subtracted by the gutters.
-     */
-    function trimToMin(sizesToTrim) {
-        // Try to get inner size of parent element.
-        // If it's no supported, return original sizes.
-        const parentSize = innerSize(parent);
-        if (parentSize === null) {
-            return sizesToTrim;
-        }
-
-        if (minSizes.reduce((a, b) => a + b, 0) > parentSize) {
-            return sizesToTrim;
-        }
-
-        // Keep track of the excess pixels, the amount of pixels over the desired percentage
-        // Also keep track of the elements with pixels to spare, to decrease after if needed
-        let excessPixels = 0;
-        const toSpare = [];
-
-        const pixelSizes = sizesToTrim.map((size, i) => {
-            // Convert requested percentages to pixel sizes
-            const pixelSize = (parentSize * size) / 100;
-            const elementGutterSize = getGutterSize(gutterSize, i === 0, i === sizesToTrim.length - 1, gutterAlign);
-            const elementMinSize = minSizes[i] + elementGutterSize;
-
-            // If element is too smal, increase excess pixels by the difference
-            // and mark that it has no pixels to spare
-            if (pixelSize < elementMinSize) {
-                excessPixels += elementMinSize - pixelSize;
-                toSpare.push(0);
-                return elementMinSize;
-            }
-
-            // Otherwise, mark the pixels it has to spare and return it's original size
-            toSpare.push(pixelSize - elementMinSize);
-            return pixelSize;
-        });
-
-        // If nothing was adjusted, return the original sizes
-        if (excessPixels === 0) {
-            return sizesToTrim;
-        }
-
-        return pixelSizes.map((pixelSize, i) => {
-            let newPixelSize = pixelSize;
-
-            // While there's still pixels to take, and there's enough pixels to spare,
-            // take as many as possible up to the total excess pixels
-            if (excessPixels > 0 && toSpare[i] - excessPixels > 0) {
-                const takenPixels = Math.min(excessPixels, toSpare[i] - excessPixels);
-
-                // Subtract the amount taken for the next iteration
-                excessPixels -= takenPixels;
-                newPixelSize = pixelSize - takenPixels;
-            }
-
-            // Return the pixel size adjusted as a percentage
-            return (newPixelSize / parentSize) * 100;
-        });
-    }
-
-    // stopDragging is very similar to startDragging in reverse.
-    function stopDragging() {
-        const self = this;
-        const a = elements[self.a].element;
-        const b = elements[self.b].element;
-
-        if (self.dragging) {
-            getOption(options, 'onDragEnd', NOOP)(getSizes());
-        }
-
-        self.dragging = false;
-
-        // Remove the stored event listeners. This is why we store them.
-        global[removeEventListener]('mouseup', self.stop);
-        global[removeEventListener]('touchend', self.stop);
-        global[removeEventListener]('touchcancel', self.stop);
-        global[removeEventListener]('mousemove', self.move);
-        global[removeEventListener]('touchmove', self.move);
-
-        // Clear bound function references
-        self.stop = null;
-        self.move = null;
-
-        a[removeEventListener]('selectstart', NOOP);
-        a[removeEventListener]('dragstart', NOOP);
-        b[removeEventListener]('selectstart', NOOP);
-        b[removeEventListener]('dragstart', NOOP);
-
-        a.style.userSelect = '';
-        a.style.webkitUserSelect = '';
-        a.style.MozUserSelect = '';
-        a.style.pointerEvents = '';
-
-        b.style.userSelect = '';
-        b.style.webkitUserSelect = '';
-        b.style.MozUserSelect = '';
-        b.style.pointerEvents = '';
-
-        self.gutter.style.cursor = '';
-        self.parent.style.cursor = '';
-        document.body.style.cursor = '';
-    }
-
-    /**
-     * startDragging calls `calculateSizes` to store the initial size in the pair object.
-     * It also adds event listeners for mouse/touch events,
-     * and prevents selection while dragging to avoid selecting text.
-     */
-    function startDragging(e) {
-        // Right-clicking can't start dragging.
-        if ('button' in e && e.button !== 0) {
-            return;
-        }
-
-        // Alias frequently used variables to save space. 200 bytes.
-        const self = this;
-        const a = elements[self.a].element;
-        const b = elements[self.b].element;
-
-        // Call the onDragStart callback.
-        if (!self.dragging) {
-            getOption(options, 'onDragStart', NOOP)(getSizes());
-        }
-
-        // Don't actually drag the element. We emulate that in the drag function.
-        e.preventDefault();
-
-        // Set the dragging property of the pair object.
-        self.dragging = true;
-
-        // Create two event listeners bound to the same pair object and store
-        // them in the pair object.
-        self.move = drag.bind(self);
-        self.stop = stopDragging.bind(self);
-
-        // All the binding. `window` gets the stop events in case we drag out of the elements.
-        global[addEventListener]('mouseup', self.stop);
-        global[addEventListener]('touchend', self.stop);
-        global[addEventListener]('touchcancel', self.stop);
-        global[addEventListener]('mousemove', self.move);
-        global[addEventListener]('touchmove', self.move);
-
-        // Disable selection. Disable!
-        a[addEventListener]('selectstart', NOOP);
-        a[addEventListener]('dragstart', NOOP);
-        b[addEventListener]('selectstart', NOOP);
-        b[addEventListener]('dragstart', NOOP);
-
-        a.style.userSelect = 'none';
-        a.style.webkitUserSelect = 'none';
-        a.style.MozUserSelect = 'none';
-        a.style.pointerEvents = 'none';
-
-        b.style.userSelect = 'none';
-        b.style.webkitUserSelect = 'none';
-        b.style.MozUserSelect = 'none';
-        b.style.pointerEvents = 'none';
-
-        // Set the cursor at multiple levels
-        self.gutter.style.cursor = cursor;
-        self.parent.style.cursor = cursor;
-        document.body.style.cursor = cursor;
-
-        // Cache the initial sizes of the pair.
-        calculateSizes.call(self);
-
-        // Determine the position of the mouse compared to the gutter
-        self.dragOffset = getMousePosition(e) - self.end;
-    }
-
-    // adjust sizes to ensure percentage is within min size and gutter.
-    sizes = trimToMin(sizes);
-
-    // 5. Create pair and element objects. Each pair has an index reference to
-    // elements `a` and `b` of the pair (first and second elements).
-    // Loop through the elements while pairing them off. Every pair gets a
-    // `pair` object and a gutter.
-    //
-    // Basic logic:
-    //
-    // - Starting with the second element `i > 0`, create `pair` objects with
-    //   `a = i - 1` and `b = i`
-    // - Set gutter sizes based on the _pair_ being first/last. The first and last
-    //   pair have gutterSize / 2, since they only have one half gutter, and not two.
-    // - Create gutter elements and add event listeners.
-    // - Set the size of the elements, minus the gutter sizes.
-    //
-    // -----------------------------------------------------------------------
-    // |     i=0     |         i=1         |        i=2       |      i=3     |
-    // |             |                     |                  |              |
-    // |           pair 0                pair 1             pair 2           |
-    // |             |                     |                  |              |
-    // -----------------------------------------------------------------------
-    const pairs = [];
-    elements = els.map((el, i) => {
-        // Create the element object.
-        const element = {
-            element: el,
-            size: sizes[i],
-            minSize: minSizes[i],
-            maxSize: maxSizes[i],
-            snapOffset: snapOffsets[i],
-            i,
-        };
-
-        let pair;
-
-        if (i > 0) {
-            // Create the pair object with its metadata.
-            pair = {
-                a: i - 1,
-                b: i,
-                dragging: false,
-                direction,
-                parent,
-            };
-
-            pair[aGutterSize] = getGutterSize(gutterSize, i - 1 === 0, false, gutterAlign);
-            pair[bGutterSize] = getGutterSize(gutterSize, false, i === els.length - 1, gutterAlign);
-
-            // if the parent has a reverse flex-direction, switch the pair elements.
-            if (parentFlexDirection === 'row-reverse' || parentFlexDirection === 'column-reverse') {
-                const temp = pair.a;
-                pair.a = pair.b;
-                pair.b = temp;
-            }
-        }
-
-        // Determine the size of the current element. IE8 is supported by
-        // staticly assigning sizes without draggable gutters. Assigns a string
-        // to `size`.
-        //
-        // Create gutter elements for each pair.
-        if (i > 0) {
-            const gutterElement = gutter(i, direction, element.element);
-            setGutterSize(gutterElement, gutterSize, i);
-
-            // Save bound event listener for removal later
-            pair[gutterStartDragging] = startDragging.bind(pair);
-
-            // Attach bound event listener
-            gutterElement[addEventListener]('mousedown', pair[gutterStartDragging]);
-            gutterElement[addEventListener]('touchstart', pair[gutterStartDragging]);
-
-            parent.insertBefore(gutterElement, element.element);
-
-            pair.gutter = gutterElement;
-        }
-
-        // After the first iteration, and we have a pair object, append it to the
-        // list of pairs.
-        if (i > 0) {
-            pairs.push(pair);
-        }
-
-        return element;
-    });
-
-    function adjustToMin(element) {
-        const isLast = element.i === pairs.length;
-        const pair = isLast ? pairs[element.i - 1] : pairs[element.i];
-
-        calculateSizes.call(pair);
-
-        const size = isLast ? pair.size - element.minSize - pair[bGutterSize] : element.minSize + pair[aGutterSize];
-
-        adjust.call(pair, size);
-    }
-
-    elements.forEach((element) => {
-        const computedSize = element.element[getBoundingClientRect]()[dimension];
-
-        if (computedSize < element.minSize) {
-            if (expandToMin) {
-                adjustToMin(element);
-            } else {
-                // eslint-disable-next-line no-param-reassign
-                element.minSize = computedSize;
-            }
-        }
-    });
-
-    function setSizes(newSizes) {
-        const trimmed = trimToMin(newSizes);
-        trimmed.forEach((newSize, i) => {
-            if (i > 0) {
-                const pair = pairs[i - 1];
-
-                const a = elements[pair.a];
-                const b = elements[pair.b];
-
-                a.size = trimmed[i - 1];
-                b.size = newSize;
-
-                setElementSize(a.element, a.size, pair[aGutterSize], a.i);
-                setElementSize(b.element, b.size, pair[bGutterSize], b.i);
-            }
-        });
-    }
-
-    function destroy(preserveStyles, preserveGutter) {
-        pairs.forEach((pair) => {
-            if (preserveGutter !== true) {
-                pair.parent.removeChild(pair.gutter);
-            } else {
-                pair.gutter[removeEventListener]('mousedown', pair[gutterStartDragging]);
-                pair.gutter[removeEventListener]('touchstart', pair[gutterStartDragging]);
-            }
-
-            if (preserveStyles !== true) {
-                const style = elementStyle(dimension, pair.a.size, pair[aGutterSize]);
-
-                Object.keys(style).forEach((prop) => {
-                    elements[pair.a].element.style[prop] = '';
-                    elements[pair.b].element.style[prop] = '';
-                });
-            }
-        });
-    }
-
-    return {
-        setSizes,
-        getSizes,
-        collapse(i) {
-            adjustToMin(elements[i]);
-        },
-        destroy,
-        parent,
-        pairs,
-    };
-};
-
-export default Split;

+ 0 - 11
src/shared/styles/_core.scss

@@ -4,17 +4,6 @@
     font-size: var(--font-size);
     direction: ltr;
 
-    .gutter {
-        background-color: #eee;
-        background-repeat: no-repeat;
-        background-position: 50%;
-
-        &.gutter-horizontal {
-            background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg==');
-            cursor: col-resize;
-        }
-    }
-
     .flyout {
         position: absolute;
     }

+ 1 - 25
src/types/plugins/muc-views/chatarea.d.ts

@@ -12,36 +12,12 @@ export default class MUCChatArea extends CustomElement {
     };
     jid: any;
     type: any;
+    split: any;
     initialize(): Promise<void>;
     model: any;
-    onMouseMove: any;
-    onMouseUp: any;
     render(): import("lit").TemplateResult<1> | "";
     shouldShowSidebar(): boolean;
     getHelpMessages(): string[];
-    /**
-     * @param {MouseEvent} ev
-     */
-    onMousedown(ev: MouseEvent): void;
-    /**
-     * @param {MouseEvent} ev
-     */
-    onStartResizeOccupants(ev: MouseEvent): void;
-    resizing: boolean;
-    width: number;
-    prev_pageX: number;
-    /**
-     * @param {MouseEvent} ev
-     */
-    _onMouseMove(ev: MouseEvent): void;
-    /**
-     * @param {MouseEvent} ev
-     */
-    _onMouseUp(ev: MouseEvent): void;
-    calculateSidebarWidth(element_position: any, delta: any): any;
-    is_minimum: boolean;
-    is_maximum: boolean;
-    resizeSidebarView(delta: any, current_mouse_position: any): void;
 }
 import { CustomElement } from 'shared/components/element.js';
 //# sourceMappingURL=chatarea.d.ts.map

+ 19 - 1
src/types/plugins/muc-views/occupant.d.ts

@@ -1,5 +1,23 @@
 export default class MUCOccupant extends CustomElement {
-    render(): import("lit").TemplateResult<1>;
+    static get properties(): {
+        muc_jid: {
+            type: StringConstructor;
+        };
+        occupant_id: {
+            type: StringConstructor;
+        };
+    };
+    muc_jid: any;
+    occupant_id: any;
+    initialize(): Promise<void>;
+    muc: any;
+    model: any;
+    render(): import("lit").TemplateResult<1> | "";
+    getVcard(): any;
+    addToContacts(): void;
+    toggleForm(ev: any): void;
+    show_role_form: any;
+    show_affiliation_form: any;
 }
 import { CustomElement } from 'shared/components/element.js';
 //# sourceMappingURL=occupant.d.ts.map

+ 1 - 1
src/types/plugins/muc-views/templates/muc-occupant.d.ts

@@ -1,3 +1,3 @@
-declare function _default(): import("lit").TemplateResult<1>;
+declare function _default(el: import("../occupant").default): import("lit").TemplateResult<1>;
 export default _default;
 //# sourceMappingURL=muc-occupant.d.ts.map

+ 1 - 1
src/types/plugins/rootview/background.d.ts

@@ -2,7 +2,7 @@ export default ConverseBackground;
 declare class ConverseBackground extends CustomElement {
     initialize(): void;
     render(): import("lit").TemplateResult<1>;
-    setClasses(): void;
+    setThemeAttributes(): void;
 }
 import { CustomElement } from 'shared/components/element.js';
 //# sourceMappingURL=background.d.ts.map

+ 1 - 1
src/types/plugins/rootview/root.d.ts

@@ -8,7 +8,7 @@
 export default class ConverseRoot extends CustomElement {
     render(): import("lit").TemplateResult<1>;
     initialize(): void;
-    setClasses(): void;
+    setThemeAttributes(): void;
 }
 import { CustomElement } from 'shared/components/element.js';
 //# sourceMappingURL=root.d.ts.map

+ 246 - 0
src/types/shared/components/split-resize.d.ts

@@ -0,0 +1,246 @@
+export default class SplitResize extends CustomElement {
+    initialize(): void;
+    pair: {
+        a: (0 | 1);
+        b: (0 | 1);
+        direction: ("horizontal" | "vertical");
+        dragging: boolean;
+        aMin: number;
+        bMin: number;
+        dragOffset: number;
+        size: number;
+        start: number;
+        end: number;
+        gutter: HTMLElement;
+        parent: HTMLElement;
+        stop: (this: Window, ev: Event) => any;
+        /**
+         * The basic sequence:
+         *
+         * 1. Set defaults to something sane. `options` doesn't have to be passed at all.
+         * 2. Initialize a bunch of strings based on the direction we're splitting.
+         *    A lot of the behavior in the rest of the library is parameterized down to
+         *    rely on CSS strings and classes.
+         * 3. Define the dragging helper functions, and a few helpers to go with them.
+         * 4. Loop through the elements while pairing them off. Every pair gets an
+         *    `pair` object and a gutter.
+         * 5. Actually size the pair elements, insert gutters and attach event listeners.
+         */
+        move: (this: Window, ev: Event) => any;
+    };
+    render(): import("lit").TemplateResult<1>;
+    /**
+     * @param {Map<string, any>} changed
+     */
+    updated(changed: Map<string, any>): void;
+    /**
+     * Helper function gets a property from the properties object, with a default fallback
+     */
+    getOption(options: any, propName: any, def: any): any;
+    getElementStyle(dim: any, size: any, gutSize: any): {};
+    defaultGutterStyleFn(dim: any, gutSize: any): {
+        [x: number]: string;
+    };
+    getGutterSize(gutterSize: any, isFirst: any, isLast: any, gutterAlign: any): any;
+    /**
+     * @param {HTMLElement} el
+     * @param {string} size
+     * @param {string} gutSize
+     */
+    setElementSize(el: HTMLElement, size: string, gutSize: string): void;
+    getSizes(): any[];
+    /**
+     * Supports touch events, but not multitouch, so only the first
+     * finger `touches[0]` is counted.
+     * @param {MouseEvent} e
+     */
+    getMousePosition(e: MouseEvent): any;
+    /**
+     * Actually adjust the size of elements `a` and `b` to `offset` while dragging.
+     * calc is used to allow calc(percentage + gutterpx) on the whole split instance,
+     * which allows the viewport to be resized without additional logic.
+     * Element a's size is the same as offset. b's size is total size - a size.
+     * Both sizes are calculated from the initial parent percentage,
+     * then the gutter size is subtracted.
+     *
+     * @param {number} offset
+     */
+    adjust(offset: number): void;
+    /**
+     * Handles the dragging logic for resizing elements.
+     *
+     * The logic is quite simple:
+     *
+     * 1. Ignore if the pair is not dragging.
+     * 2. Get the offset of the event.
+     * 3. Snap offset to min if within snappable range (within min + snapOffset).
+     * 4. Actually adjust each element in the pair to offset.
+     *
+     * ---------------------------------------------------------------------
+     * |    | <- a.minSize               ||              b.minSize -> |    |
+     * |    |  | <- this.snapOffset      ||     this.snapOffset -> |  |    |
+     * |    |  |                         ||                        |  |    |
+     * |    |  |                         ||                        |  |    |
+     * ---------------------------------------------------------------------
+     * | <- this.start                                        this.size -> |
+     *
+     * @param {MouseEvent} e
+     * @param {object} options
+     */
+    drag(e: MouseEvent, options: object): void;
+    /**
+     * Cache some important sizes when drag starts, so we don't have to do that
+     * continuously:
+     *
+     * `size`: The total size of the pair. First + second + first gutter + second gutter.
+     * `start`: The leading side of the first element.
+     *
+     * ------------------------------------------------
+     * |      aGutterSize -> |||                      |
+     * |                     |||                      |
+     * |                     |||                      |
+     * |                     ||| <- bGutterSize       |
+     * ------------------------------------------------
+     * | <- start                             size -> |
+     *
+     * @param {ResizablePair} pair
+     */
+    calculateSizes(pair: {
+        a: (0 | 1);
+        b: (0 | 1);
+        direction: ("horizontal" | "vertical");
+        dragging: boolean;
+        aMin: number;
+        bMin: number;
+        dragOffset: number;
+        size: number;
+        start: number;
+        end: number;
+        gutter: HTMLElement;
+        parent: HTMLElement;
+        stop: (this: Window, ev: Event) => any;
+        /**
+         * The basic sequence:
+         *
+         * 1. Set defaults to something sane. `options` doesn't have to be passed at all.
+         * 2. Initialize a bunch of strings based on the direction we're splitting.
+         *    A lot of the behavior in the rest of the library is parameterized down to
+         *    rely on CSS strings and classes.
+         * 3. Define the dragging helper functions, and a few helpers to go with them.
+         * 4. Loop through the elements while pairing them off. Every pair gets an
+         *    `pair` object and a gutter.
+         * 5. Actually size the pair elements, insert gutters and attach event listeners.
+         */
+        move: (this: Window, ev: Event) => any;
+    }): void;
+    /**
+     * @param {HTMLElement} el
+     */
+    innerSize(el: HTMLElement): any;
+    /**
+     * When specifying percentage sizes that are less than the computed
+     * size of the element minus the gutter, the lesser percentages must be increased
+     * (and decreased from the other elements) to make space for the pixels
+     * subtracted by the gutters.
+     */
+    trimToMin(sizesToTrim: any): any;
+    /**
+     * stopDragging is very similar to startDragging in reverse.
+     * @param {Object} options
+     */
+    stopDragging(options: any): void;
+    /**
+     * startDragging calls `calculateSizes` to store the initial size in the pair object.
+     * It also adds event listeners for mouse/touch events,
+     * and prevents selection while dragging to avoid selecting text.
+     * @param {MouseEvent} e
+     * @param {Object} options
+     */
+    startDragging(e: MouseEvent, options: any): void;
+    /**
+     * @param {Object} element
+     */
+    adjustToMin(element: any): void;
+    /**
+     * @param {Array<number>} newSizes
+     */
+    setSizes(newSizes: Array<number>): void;
+    /**
+     * The main function to initialize a split.
+     *
+     * Each pair of elements, resizable relative to one another, is handled independently.
+     * Dragging the gutter between two elements only changes the dimensions of elements in that pair.
+     *
+     * A pair object is shaped like this:
+     *
+     * @typedef {Object} ResizablePair
+     * @property {(0|1)} a
+     * @property {(0|1)} b
+     * @property {('horizontal'|'vertical')} direction
+     * @property {boolean} dragging
+     * @property {number} aMin
+     * @property {number} bMin
+     * @property {number} dragOffset
+     * @property {number} size
+     * @property {number} start
+     * @property {number} end
+     * @property {HTMLElement} gutter
+     * @property {HTMLElement} parent
+     * @property {(this: Window, ev: Event) => any} stop
+     * @property {(this: Window, ev: Event) => any} move
+     *
+     * The basic sequence:
+     *
+     * 1. Set defaults to something sane. `options` doesn't have to be passed at all.
+     * 2. Initialize a bunch of strings based on the direction we're splitting.
+     *    A lot of the behavior in the rest of the library is parameterized down to
+     *    rely on CSS strings and classes.
+     * 3. Define the dragging helper functions, and a few helpers to go with them.
+     * 4. Loop through the elements while pairing them off. Every pair gets an
+     *    `pair` object and a gutter.
+     * 5. Actually size the pair elements, insert gutters and attach event listeners.
+     *
+     * @param {HTMLElement[]} els
+     */
+    setupSplit(els: HTMLElement[], options?: {}): void;
+    parent: HTMLElement;
+    minSizes: any[];
+    gutterSize: any;
+    gutterAlign: any;
+    dragInterval: any;
+    direction: any;
+    cursor: any;
+    dimension: string;
+    clientAxis: string;
+    position: string;
+    positionEnd: string;
+    clientSize: string;
+    elements: {
+        element: HTMLElement;
+        size: any;
+        minSize: any;
+        maxSize: any;
+        snapOffset: any;
+        i: number;
+    }[];
+    /**
+     * @param {HTMLElement[]} els
+     * @param {HTMLElement} el
+     * @param {number} i
+     * @param {object} options
+     */
+    createElement(els: HTMLElement[], el: HTMLElement, i: number, options: object): {
+        element: HTMLElement;
+        size: any;
+        minSize: any;
+        maxSize: any;
+        snapOffset: any;
+        i: number;
+    };
+    /**
+     * @param {object} options
+     */
+    createPair(options: object): void;
+}
+import { CustomElement } from './element.js';
+//# sourceMappingURL=split-resize.d.ts.map