src/utils/dom.js
/*
* OS.js - JavaScript Cloud/Web Desktop Platform
*
* Copyright (c) Anders Evenrud <andersevenrud@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* @author Anders Evenrud <andersevenrud@gmail.com>
* @license Simplified BSD License
*/
import logger from '../logger';
const supportsNativeNotification = 'Notification' in window;
/**
* Creates a new CSS DOM element
* @param {Element} root Root node
* @param {string} src Source
* @return {Promise<ScriptElement>}
*/
export const style = (root, src) => new Promise((resolve, reject) => {
const el = document.createElement('link');
el.setAttribute('rel', 'stylesheet');
el.setAttribute('type', 'text/css');
el.onload = () => resolve(el);
el.onerror = (err) => reject(err);
el.setAttribute('href', src);
root.appendChild(el);
return el;
});
/**
* Creates a new Script DOM element
* @param {Element} root Root node
* @param {string} src Source
* @param {Object} [options={}] Options
* @return {Promise<StyleElement>}
*/
export const script = (root, src, options = {}) => new Promise((resolve, reject) => {
const opts = {
async: false,
defer: false,
...options,
src,
onerror: (err) => reject(err),
onload: () => resolve(el),
};
const el = document.createElement('script');
el.onreadystatechange = function() {
if ((this.readyState === 'complete' || this.readyState === 'loaded')) {
resolve(el);
}
};
Object.assign(el, opts);
root.appendChild(el);
return el;
});
/**
* Escape text so it is "safe" for HTML usage
* @param {string} text Input text
* @return {string}
*/
export const escapeHtml = (text) => {
const div = document.createElement('div');
div.innerHTML = text;
return div.textContent;
};
/**
* Serialize an object to CSS
* @param {object} obj Object
* @return {string} CSS text
*/
export const createCssText = (obj) => Object.keys(obj)
.map(k => [k, k.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()])
.map(k => `${k[1]}: ${obj[k[0]]}`)
.join(';');
/**
* Inserts a tab in the given event target
* @param {Event} ev DOM Event
*/
export const handleTabOnTextarea = ev => {
const input = ev.target;
let {selectionStart, selectionEnd, value} = input;
input.value = value.substring(0, selectionStart)
+ '\t'
+ value.substring(selectionEnd, value.length);
selectionStart++;
input.selectionStart = selectionStart;
input.selectionEnd = selectionStart;
};
/*
* Get active element if belonging to root
* @param {Element} root DOM Element
* @return {Element|null}
*/
export const getActiveElement = (root) => {
if (root) {
const ae = document.activeElement;
return root.contains(ae) ? ae : null;
}
return null;
};
/**
* Checks if passive events is supported
* @link https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md
* @return {boolean}
*/
export const supportsPassive = (function() {
let supportsPassive = false;
try {
const opts = Object.defineProperty({}, 'passive', {
get: () => (supportsPassive = true)
});
window.addEventListener('testPassive', null, opts);
window.removeEventListener('testPassive', null, opts);
} catch (e) {/* noop */}
return () => supportsPassive;
})();
/**
* Plays a sound
* @param {string} src Sound source
* @param {object} [options] Options
* @return {Promise<HTMLAudioElement>}
*/
export const playSound = (src, options = {}) => {
const opts = {
volume: 1.0,
...options
};
const audio = new Audio();
audio.volume = opts.volume;
audio.src = src;
try {
const p = audio.play();
if (p instanceof Promise) {
return p.then(() => audio)
.catch(err => logger.warn('Failed to play sound', src, err));
}
} catch (e) {
logger.warn('Failed to play sound', src, e);
}
return Promise.resolve(audio);
};
/**
* Gets supported media types
* @return {object}
*/
export const supportedMedia = () => {
const videoFormats = {
mp4: 'video/mp4',
ogv: 'video/ogg'
};
const audioFormats = {
mp3: 'audio/mpeg',
mp4: 'audio/mp4',
oga: 'audio/ogg'
};
const reduce = (list, elem) => Object.keys(list)
.reduce((result, format) => {
return {
[format]: elem.canPlayType(list[format]) === 'probably',
...result
};
}, {});
return {
audio: reduce(audioFormats, document.createElement('audio')),
video: reduce(videoFormats, document.createElement('video'))
};
};
/**
* Gets if CSS transitions is supported
* @return {boolean}
*/
export const supportsTransition = (function() {
const el = document.createElement('div');
const tests = ['WebkitTransition', 'MozTransition', 'OTransition', 'transition'];
const supported = tests.some(name => typeof el.style[name] !== 'undefined');
return () => supported;
})();
/**
* Creates a native notification
* @param {object} options Notification options
* @param {Function} [onclick] Callback on click
* @return {Promise<window.Notification>}
*/
export const createNativeNotification = (options, onclick) => {
const Notif = window.Notification;
const create = () => {
const notification = new Notif(
options.title,
{
body: options.message,
icon: options.icon
}
);
notification.onclick = onclick;
return notification;
};
if (supportsNativeNotification) {
if (Notif.permission === 'granted') {
return Promise.resolve(create());
} else if (Notif.permission !== 'denied') {
return new Promise((resolve, reject) => {
Notif.requestPermission(permission => {
return permission === 'granted' ? resolve(true) : reject(permission);
});
}).then(create);
}
}
return Promise.reject('Unsupported');
};