1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-08-12 09:00:54 +00:00
FrankerFaceZ/src/utilities/dom.js
SirStendec ff0f0ea074 4.16.0
* Added: Show the FFZ menu button on new dashboard pages.
* Fixed: Synchronize settings with the `dashboard.twitch.tv` subdomain.
* Fixed: Performance issue with metadata tool-tips being calculated too frequently.
* Fixed: Metadata not appearing in theater mode when portrait mode is enabled.
* API Added: Chat Action types can now override rendering.
2019-11-25 17:50:20 -05:00

284 lines
No EOL
6.5 KiB
JavaScript

'use strict';
import {has} from 'utilities/object';
const ATTRS = [
'accept', 'accept-charset', 'accesskey', 'action', 'align', 'alt', 'async',
'autocomplete', 'autofocus', 'autoplay', 'bgcolor', 'border', 'buffered',
'challenge', 'charset', 'checked', 'cite', 'class', 'code', 'codebase',
'color', 'cols', 'colspan', 'content', 'contenteditable', 'contextmenu',
'controls', 'coords', 'crossorigin', 'data', 'data-*', 'datetime',
'default', 'defer', 'dir', 'dirname', 'download', 'draggable',
'dropzone', 'enctype', 'for', 'form', 'formaction', 'headers', 'height',
'hidden', 'high', 'href', 'hreflang', 'http-equiv', 'icon', 'id',
'integrity', 'ismap', 'itemprop', 'keytype', 'kind', 'label', 'lang',
'language', 'list', 'loop', 'low', 'manifest', 'max', 'maxlength',
'minlength', 'media', 'method', 'min', 'multiple', 'muted', 'name',
'novalidate', 'open', 'optimum', 'pattern', 'ping', 'placeholder', 'poster',
'preload', 'radiogroup', 'readonly', 'rel', 'required', 'reversed', 'rows',
'rowspan', 'sandbox', 'scope', 'scoped', 'seamless', 'selected', 'shape',
'size', 'sizes', 'slot', 'span', 'spellcheck', 'src', 'srcdoc', 'srclang',
'srcset', 'start', 'step', 'style', 'summary', 'tabindex', 'target',
'title', 'type', 'usemap', 'value', 'width', 'wrap'
];
const range = document.createRange();
function camelCase(name) {
return name.replace(/[-_]\w/g, m => m[1].toUpperCase());
}
export function on(obj, ...args) {
return obj.addEventListener(...args);
}
export function off(obj, ...args) {
return obj.removeEventListener(...args);
}
export function findReactFragment(frag, criteria, depth = 25, current = 0, visited = null) {
if ( ! visited )
visited = new Set;
else if ( visited.has(frag) )
return null;
if ( criteria(frag) )
return frag;
if ( current >= depth )
return null;
visited.add(frag);
if ( frag && frag.props && Array.isArray(frag.props.children) )
for(const child of frag.props.children) {
if ( ! child )
continue;
if ( Array.isArray(child) ) {
for(const f of child) {
const out = findReactFragment(f, criteria, depth, current + 1, visited);
if ( out )
return out;
}
} else {
const out = findReactFragment(child, criteria, depth, current + 1, visited);
if ( out )
return out;
}
}
return null;
}
export function createElement(tag, props, ...children) {
const el = document.createElement(tag);
if ( children.length === 1)
children = children[0];
if ( typeof props === 'string' )
el.className = props;
else if ( props )
for(const key in props)
if ( has(props, key) ) {
const lk = key.toLowerCase(),
prop = props[key];
if ( lk === 'style' ) {
if ( typeof prop === 'string' )
el.style.cssText = prop;
else
for(const k in prop)
if ( has(prop, k) )
el.style[k] = prop[k];
} else if ( lk === 'dataset' ) {
for(const k in prop)
if ( has(prop, k) )
el.dataset[camelCase(k)] = prop[k];
} else if ( key === 'dangerouslySetInnerHTML' ) {
// React compatibility is cool. SeemsGood
if ( prop && prop.__html )
el.innerHTML = prop.__html;
} else if ( lk.startsWith('on') )
el.addEventListener(lk.slice(2), prop);
else if ( lk.startsWith('data-') )
el.dataset[camelCase(lk.slice(5))] = prop;
else if ( lk.startsWith('aria-') || ATTRS.includes(lk) )
el.setAttribute(key, prop);
else
el[key] = props[key];
}
if ( children )
setChildren(el, children);
return el;
}
export function setChildren(el, children, no_sanitize, no_empty) {
if ( typeof children === 'string' ) {
if ( no_empty ) {
el.appendChild(no_sanitize ?
range.createContextualFragment(children) :
document.createTextNode(children)
)
} else {
if ( no_sanitize )
el.innerHTML = children;
else
el.textContent = children;
}
} else if ( Array.isArray(children) ) {
if ( ! no_empty )
el.innerHTML = '';
for(const child of children)
if ( typeof child === 'string' )
el.appendChild(no_sanitize ?
range.createContextualFragment(child) :
document.createTextNode(child)
);
else if ( Array.isArray(child) )
setChildren(el, child, no_sanitize, true);
else if ( child )
el.appendChild(child);
} else if ( children ) {
if ( ! no_empty )
el.innerHTML = '';
el.appendChild(children);
}
}
export function findSharedParent(element, other, selector) {
while(element) {
if ( element.contains(other) )
return true;
element = element.parentElement;
if ( selector )
element = element && element.closest(selector);
}
return false;
}
export function openFile(contentType, multiple) {
return new Promise(resolve => {
const input = document.createElement('input');
input.type = 'file';
input.accept = contentType;
input.multiple = multiple;
input.onchange = () => {
const files = Array.from(input.files);
resolve(multiple ? files : files[0])
}
input.click();
})
}
export function readFile(file, encoding = 'utf-8') {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsText(file, encoding);
reader.onload = () => resolve(reader.result);
reader.onerror = e => reject(e);
});
}
const el = createElement('span');
export function sanitize(text) {
el.textContent = text;
return el.innerHTML;
}
let last_id = 0;
export class ManagedStyle {
constructor(id) {
this.id = id || last_id++;
this._blocks = {};
this._style = createElement('style', {
type: 'text/css',
id: `ffz--managed-style--${this.id}`
});
document.head.appendChild(this._style);
}
destroy() {
this._style.remove();
this._blocks = null;
this._style = null;
}
set(key, value, force) {
const block = this._blocks[key];
if ( block ) {
if ( ! force && block.textContent === value )
return;
block.textContent = value;
} else
this._style.appendChild(this._blocks[key] = document.createTextNode(value));
}
delete(key) {
const block = this._blocks[key];
if ( block ) {
if ( this._style.contains(block) )
this._style.removeChild(block);
this._blocks[key] = null;
}
}
}
export class ClickOutside {
constructor(element, callback) {
this.el = element;
this.cb = callback;
this._fn = this.handleClick.bind(this);
document.documentElement.addEventListener('click', this._fn);
}
destroy() {
if ( this._fn )
document.documentElement.removeEventListener('click', this._fn);
this.cb = this.el = this._fn = null;
}
handleClick(e) {
if ( this.el && ! this.el.contains(e.target) )
this.cb(e);
}
}