TouchDevelop/rt/html.ts

1329 строки
46 KiB
TypeScript

///<reference path='refs.ts'/>
module TDev.HTML {
export function tr(parent: HTMLElement, cl: string) {
var d = document.createElement('tr');
d.className = cl;
parent.appendChild(d);
return d;
}
export function td(parent: HTMLElement, cl: string) {
var d = document.createElement('td');
d.className = cl;
parent.appendChild(d);
return d;
}
export function col(parent: HTMLElement) {
var d = document.createElement('col');
parent.appendChild(d);
return d;
}
export function jsrequireAsync(url: string): Promise {
return new Promise((onSuccess, onProgress, onError) => {
// look for previous script tag
if (Util.children(document.head).some(el => /script/i.test(el.tagName) && el.getAttribute("src") == url)) {
onSuccess(undefined);
return;
}
Util.log('require ' + url);
// add a new script entry and wait till loaded
var script = <HTMLScriptElement> document.createElement("script");
script.type = "text/javascript";
script.charset = "utf-8";
script.onload = () => {
if (!script.readyState || script.readyState === 'complete') {
Util.log('require success ' + url);
onSuccess(undefined);
}
};
script.onreadystatechange = script.onload;
script.onerror = (err) => {
Util.log('require error: {0}', err);
onSuccess(err)
};
script.src = url;
document.head.appendChild(script);
});
}
export interface OEmbed {
title: string;
author_name: string;
author_url: string;
html: string;
thumbnail_url: string;
provider_name: string;
provider_url: string;
}
export function mkOEmbed(url: string, oe: OEmbed): HTMLElement {
var d = div('md-video-link',
div('', HTML.mkImg(oe.thumbnail_url)).withClick(() => window.open(url, 'oembed') ),
oe.title,
HTML.mkA('', oe.author_url, 'oembed', oe.author_name),
HTML.mkA('', oe.provider_url, 'oembed', oe.provider_url)
);
return d;
}
export function mkLazyVideoPlayer(preview: string, iframeSrc:string): HTMLElement {
var d = div('md-video-link');
Browser.setInnerHTML(d, SVG.getVideoPlay(preview));
d.setAttribute("data-playersrc", iframeSrc);
d.withClick(() => {
d.innerHTML = Util.fmt("<div class='md-video-wrapper'><iframe src='{0:url}' frameborder='0' allowfullscreen=''></iframe></div>",iframeSrc);
});
return d;
}
export function mkYouTubePlayer(ytid: string) {
return mkLazyVideoPlayer(
Util.fmt('https://img.youtube.com/vi/{0:q}/mqdefault.jpg', ytid),
Util.fmt("//www.youtube-nocookie.com/embed/{0:uri}?modestbranding=1&autoplay=1&autohide=1&origin={1:uri}", ytid, Cloud.config.rootUrl)
);
}
export function mkAudio(url: string, aacUrl: string = null, mp3Url: string = null, controls = false): HTMLAudioElement {
var audio = <HTMLAudioElement>document.createElement('audio');
(<any>audio).crossorigin = "anonymous";
audio.controls = controls;
setAudioSource(audio, url, aacUrl, mp3Url);
return audio;
}
export function audioLoadAsync(audio: HTMLAudioElement): Promise {
return new Promise((onSuccess, onError, onProgress) => {
audio.oncanplay = () => {
Util.log('loaded sound oncanplay');
audio.oncanplay = null;
audio.oncanplaythrough = null;
audio.onerror = null;
onSuccess(audio);
};
audio.oncanplaythrough = () => {
Util.log('loaded sound oncanplaythrough');
audio.oncanplay = null;
audio.oncanplaythrough = null;
audio.onerror = null;
onSuccess(audio);
};
audio.onerror = (e: Event) => {
Util.log('failed loading sound - ' + audio.readyState);
audio.oncanplay = null;
audio.oncanplaythrough = null;
audio.onerror = null;
onSuccess(audio);
};
// poll for browsers who don't implement events properly
var retry = 20;
var loadTracker = () => {
var readyState = <number>(<any>(audio.readyState));
if (!audio.oncanplay) return;
if (readyState === HTMLMediaElement.HAVE_ENOUGH_DATA) {
audio.oncanplay = null;
audio.oncanplaythrough = null;
audio.onerror = null;
onSuccess(audio);
} else if (retry-- > 0) {
Util.log('retry loading sound. readState:' + readyState + ', networkState:' + audio.networkState + ', try:' + retry);
Util.setTimeout(250, loadTracker);
}
else { // give up
Util.log('timeout loading sound');
audio.oncanplay = null;
audio.oncanplaythrough = null;
audio.onerror = null;
onSuccess(audio);
}
};
try {
Util.log('start loading sound');
audio.load();
Util.setTimeout(400, loadTracker);
}
catch (e) {
Util.log('failed loading sound: ' + e.message);
onSuccess(audio);
}
});
}
export function setAudioSource(audio: HTMLAudioElement, url: string, aacUrl: string = null, mp3Url: string = null) {
if (!url) {
audio.setChildren([]);
return;
}
// special handling of data urls
var m = url.match(/^data:audio\/(mp3|mp4|wav);base64,/i);
if (m) {
Util.log('audio: src datauri ' + m[1]);
var src = <HTMLSourceElement>document.createElement('source');
src.src = url;
src.type = 'audio/' + m[1];
audio.setChildren([src]);
}
else {
// in general, we don't know if the file is a wav or a mp3
var wavSrc = <HTMLSourceElement>document.createElement('source');
wavSrc.src = proxyResource(url);
wavSrc.type = 'audio/wav';
var mp3Src = <HTMLSourceElement>document.createElement('source');
mp3Src.src = proxyResource(mp3Url || url);
mp3Src.type = 'audio/mp3';
var aacSrc = <HTMLSourceElement>document.createElement('source');
aacSrc.src = proxyResource(aacUrl || url);
aacSrc.type = 'audio/mp4';
audio.setChildren([aacSrc, wavSrc, mp3Src]);
}
}
export interface ITextTrackCue {
startTime: number;
endTime: number;
message: string
}
export function parseWtt(wtt: string): ITextTrackCue[] {
var r = []
if (wtt) {
try {
var rx = /((\d{2}):)?(\d{2}):(\d{2})\.(\d{3}) --> ((\d{2}):)?(\d{2}):(\d{2})\.(\d{3})/gi;
var m: RegExpExecArray;
while (m = rx.exec(wtt)) {
var startTime = parseInt(m[2] || "0") * 3600 + parseInt(m[3]) * 60 + parseInt(m[4]) + parseInt(m[5]) / 1000;
var endTime = parseInt(m[7] || "0") * 3600 + parseInt(m[8]) * 60 + parseInt(m[9]) + parseInt(m[10]) / 1000;
var message = wtt.substr(m.index + m[0].length).trim();
var emptyLine = /^$/m.exec(message);
if (emptyLine) message = message.substr(0, emptyLine.index).trim();
r.push({ startTime: startTime, endTime: endTime, message: message });
}
}
catch (e) {
Util.reportError("wtt", e, false);
return r;
}
}
return r;
}
export function pauseVideos(el: HTMLElement) {
if (el) {
var vids = el.getElementsByTagName("video");
for (var i = 0; i < vids.length; ++i) {
try { vids.item(i).pause(); } catch (e) { }
}
}
}
export function patchWavToMp4Url(url: string): string {
if (url) {
var m = url.match(/^http(s?):\/\/(cdn\.touchdevelop\.com|az31353\.vo\.msecnd\.net)\/pub\/(\w+)/i);
if (m) return 'https://' + m[2] + '/aac/' + m[3] + '.m4a';
if (/^\.\/art\//i.test(url)) return url + '.m4a';
}
return url;
}
export function mkBr() { return document.createElement("br") }
export function mkTextArea(cls : string = null) {
var ta = <HTMLTextAreaElement>document.createElement("textarea");
if (cls != null)
ta.className = cls;
dirAuto(ta);
ta.onselectstart = (e) => {
e.stopImmediatePropagation();
return true;
}
return ta;
}
export function setupDragAndDrop(r: HTMLElement, onFiles : (files : FileList) => void) {
if (!Browser.dragAndDrop) return;
r.addEventListener('dragover', function (e) {
if (e.dataTransfer.types[0] == 'Files') {
if (e.preventDefault) e.preventDefault(); // Necessary. Allows us to drop.
e.dataTransfer.dropEffect = 'copy'; // See the section on the DataTransfer object.
return false;
}
}, false);
r.addEventListener('drop',(e) => {
if (e.dataTransfer.files[0]) {
e.stopPropagation(); // Stops some browsers from redirecting.
e.preventDefault();
onFiles(e.dataTransfer.files);
}
return false;
}, false);
r.addEventListener('dragend',(e) => {
return false;
}, false);
}
export function mkButtonElt(cl:string, ...children:any[]) {
var elt = <HTMLElement> document.createElement("button");
if (cl != null)
elt.className = cl;
elt.appendChildren(children);
dirAuto(elt);
return elt;
}
export function mkImg(url:string, cls : string = undefined):HTMLElement {
if (/^\//.test(url))
url = (<any> url).slice(1);
var m = /^scripticons(96)?\/(.*)\.png/.exec(url);
if (m) {
url = "svg:" + m[2] + ",white";
}
var img;
if (/^svg:/.test(url)) {
img = SVG.getIconSVG(url.slice(4));
} else {
var elt = <HTMLImageElement> document.createElement("img");
elt.src = proxyResource(url);
elt.alt = "";
img = elt;
}
if (cls)
img.className += " " + cls;
return img;
}
export function mkImgButton(img:string, f:()=>void)
{
var i = HTML.mkImg(img);
HTML.setRole(i, "presentation");
var btn = mkButtonElt("wall-button", i);
Util.clickHandler(btn, f);
return btn;
}
export function mkDisablableButton(content:string, f:()=>void)
{
var r = mkButton(content, () => {
if (!r.getFlag("disabled")) f();
})
return r;
}
export function mkAsyncButton(content:string, f:()=>Promise, cls = ""):HTMLElement
{
var btn = mkButtonElt("wall-button " + cls, text(content));
var running = false
Util.clickHandler(btn, () => {
if (running) return
running = true
btn.style.opacity = "0.5"
btn.setFlag("disabled", true)
f().done(() => {
running = false
btn.style.opacity = null
btn.setFlag("disabled", false)
})
});
return btn;
}
export function mkButton(content:string, f:()=>void, cls = ""):HTMLElement
{
var btn = mkButtonElt("wall-button " + cls, text(content));
Util.clickHandler(btn, f);
return btn;
}
export function mkLinkButton(content:string, f:()=>void, cls = "")
{
var btn = mkButtonElt("link-button " + cls, text(content));
Util.clickHandler(btn, f);
return btn;
}
export function mkButtonTick(content:string, t:Ticks, f:()=>void, cls = "")
{
var btn = mkButtonElt("wall-button " + cls, text(content));
setTickCallback(btn, t, f);
return btn;
}
export function mkButtonOnce(content:string, f:()=>void, removeSiblings : boolean = false)
{
var btn = mkButtonElt("wall-button", text(content));
Util.clickHandler(btn, (e) => {
if (removeSiblings)
(<Element>btn.parentNode).removeAllChildren();
else
btn.removeSelf();
f();
});
return btn;
}
export function setTickCallback(btn:HTMLElement, tick:Ticks, f:()=>void)
{
if (tick == Ticks.noEvent) {
return btn.withClick(f)
} else {
btn.id = "btn-" + Ticker.tickName(tick);
return btn.withClick(() => { Ticker.tick(tick); f() })
}
return btn
}
export function mkRoundButton(icon:string, name:string, tick:Ticks, f:()=>void) :HTMLButtonElement
{
var btn = HTML.mkButtonElt("topMenu-button " + (name.length > 11 ? "topMenu-button-long-desc" : ""), [
div("topMenu-button-frame", HTML.mkImg(icon)),
div("topMenu-button-desc", name)
]);
setTickCallback(btn, tick, f)
return <HTMLButtonElement>btn;
}
export interface IInputElement {
element: HTMLElement;
validate(): string; // returns error message
readAsync(): Promise; // of string
}
export function fileReadAsDataURLAsync(f: File) : Promise {
if (!f)
return Promise.as(null);
else {
return new Promise((onSuccess, onError, onProgress) => {
var reader = new FileReader();
reader.onerror = (ev) => onSuccess(null);
reader.onload = (ev) => onSuccess(reader.result);
reader.readAsDataURL(f);
});
}
}
export var documentMimeTypes: StringMap<string> = {
"text/css": "css",
"application/javascript": "js",
"text/plain": "txt",
"application/pdf": "pdf",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx",
"application/vnd.openxmlformats-officedocument.presentationml.presentation": "pptx"
};
export function mkDocumentInput(maxMb: number): IInputElement {
var input = HTML.mkTextInput("file", lf("choose a file"));
input.accept = Object.keys(documentMimeTypes).join(";");
return <IInputElement>{
element: input,
validate: function (): string {
var f = input.files[0];
if (!f)
return 'Oops, you need to select a file...';
if (maxMb > 0 && f.size > maxMb * 1000000)
return 'Sorry, the file is too big. The sound must be less than ' + maxMb + 'Mb...';
if (input.accept.indexOf(f.type) < 0)
return 'Sorry, this document format is not supported...';
return null;
},
readAsync: () => fileReadAsDataURLAsync(input.files[0])
};
}
export var mkAudioInput = (allowEmpty: boolean, maxMb: number): IInputElement =>
{
var input = HTML.mkTextInput("file", lf("choose a file"));
input.accept = "audio/wav";
return <IInputElement>{
element: input,
validate: function (): string {
var files = input.files;
if (files.length == 0)
return allowEmpty ? null : 'Oops, you need to select a sound...';
var f = files[0];
if (maxMb > 0 && f.size > maxMb * 1000000)
return 'Sorry, the sound is too big. The sound must be less than ' + maxMb + 'Mb...';
if (f.type !== 'audio/wav' && f.type !== 'audio/x-wav') // audio/x-wav on Mac/Safari
return 'Sorry, you can only upload WAV sounds...';
return null;
},
readAsync: () => fileReadAsDataURLAsync(input.files[0])
};
}
export var mkImageChooser = (onchanged:(dataUri:string)=>void):HTMLElement =>
{
var file = HTML.mkTextInput("file", lf("choose a picture"));
file.accept = "image/jpeg,image/png";
file.onchange = () => {
var f = file.files.length > 0 ? file.files[0] : null;
if (!f) return;
var reader = new FileReader();
reader.onload = (ev) => onchanged(reader.result);
reader.readAsDataURL(f);
};
return file;
}
export function mkFileInput(file : File, maxMb: number): IInputElement
{
var input;
if (/^image\//.test(file.type)) {
input = document.createElement("img");
input.style.maxWidth = '21em';
input.style.maxHeight = '11em';
input.src = file;
fileReadAsDataURLAsync(file).done(url => input.src = url);
} else if (/^audio\//.test(file.type)) {
input = document.createElement("audio");
(<any>input).crossorigin = "anonymous";
input.src = file;
fileReadAsDataURLAsync(file).done(url => input.src = url);
} else {
input = div('wall-textbox', lf("{0} {1}Kb", file.name, Math.ceil(file.size / 1000)));
}
input.style.margins = '0.5em';
return <IInputElement>{
element : input,
validate : () => null,
readAsync: (): Promise => fileReadAsDataURLAsync(file)
};
}
export var mkImageInput = (allowEmpty : boolean, maxMb: number): IInputElement =>
{
var input = HTML.mkTextInput("file", lf("choose a picture"));
input.accept = "image/jpeg,image/png";
return <IInputElement>{ element : input,
validate : function (): string {
var files = input.files;
if (files.length == 0)
return allowEmpty ? null : lf("Oops, you need to select a picture...");
var f = files[0];
if (maxMb > 0 && f.size > maxMb * 1000000)
return lf("Sorry, the picture is too big. The picture must be less than {0} Mb...", maxMb);
if (f.type !== 'image/jpeg' && f.type !== 'image/png')
return lf("Sorry, you can only upload JPEG and PNG pictures...");
return null;
},
readAsync : function (): Promise { // of String
var f = input.files[0];
if (!f)
return Promise.as(null);
else {
return new Promise((onSuccess, onError, onProgress) => {
var reader = new FileReader();
reader.onerror = (ev) => onSuccess(null);
reader.onload = (ev) => onSuccess(reader.result);
reader.readAsDataURL(f);
});
}
}
};
}
export function setRole(el: HTMLElement, role: string) {
if (!el) return;
if (role)
el.setAttribute("role", role);
else
el.removeAttribute("role");
}
export function enableSpeech(el: HTMLInputElement, changed: () => void ) {
el.setAttribute('x-webkit-speech', 'x-webkit-speech');
(<any>el).onwebkitspeechchange = () => {
changed();
};
(<any>el).onspeechchange = () => {
changed();
};
}
export function mkTextInput(type:string, placeholder : string, role?:string) : HTMLInputElement
{
var txt = <HTMLInputElement> document.createElement("input");
txt.setAttribute("type", type);
if (placeholder) {
txt.setAttribute("placeholder", placeholder);
txt.setAttribute("aria-label", placeholder);
}
if (role) HTML.setRole(txt, role);
txt.autofocus = false;
txt.className = "wall-textbox";
dirAuto(txt);
//https://developer.apple.com/library/safari/codinghowtos/Mobile/UserExperience/_index.html#//apple_ref/doc/uid/DTS40008248-CH1-DontLinkElementID_13
if (Browser.browser == BrowserSoftware.safari)
(<any>txt).autocapitalize = false;
txt.onselectstart = (e) => {
e.stopImmediatePropagation();
return true;
}
return txt;
}
export function mkTextInputWithOk(type:string, placeholder? : string, onOk? : () => void) : HTMLInputElement
{
var res = mkTextInput(type, placeholder)
var okBtn:HTMLElement = null;
Util.onInputChange(res, () => {
if (okBtn) return
res.style.width = "calc(100% - 6em)";
okBtn = mkButton(lf("ok"), () => {
var b = okBtn
okBtn = null
res.style.width = "";
if (b) b.removeSelf();
res.blur()
if (onOk) onOk();
}, "input-confirm");
res.parentNode.insertBefore(okBtn, res.nextSibling)
})
res.addEventListener("blur", () => {
var b = okBtn
okBtn = null
res.style.width = "";
if (b) b.removeSelf();
}, false)
return res
}
export function mkOption(value: string, label: string, selected: boolean = undefined, ...children: any[]): HTMLOptionElement {
var option = <HTMLOptionElement> document.createElement("option");
option.label = label;
option.value = value;
if (selected !== undefined) option.selected = selected;
if (label) option.appendChildren(label);
option.appendChildren(children);
return option;
}
export function mkComboBox(options: HTMLOptionElement[]): HTMLSelectElement {
var combobox = <HTMLSelectElement> document.createElement("select");
combobox.autofocus = false;
combobox.className = "wall-textbox";
combobox.appendChildren(options);
return combobox;
}
export function getCheckboxValue(ch: HTMLElement) { return !!(<any> ch).selected; }
export function setCheckboxValue(ch:HTMLElement, v:boolean)
{
(<any> ch).theBox.setChildren(v ? [text("\u2713")] : []);
(<any> ch).selected = v;
}
export function mkCheckBox(lbl:string, onchg:(v:boolean)=>void = undefined, v?:boolean)
{
return mkTickCheckBox(Ticks.noEvent, lbl, onchg, v)
}
export function mkTickCheckBox(t:Ticks, lbl:string, onchg:(v:boolean)=>void = undefined, v?:boolean)
{
var b = div("theBox", text(""));
var r = div("checkbox", b, text(lbl));
(<any> r).theBox = b;
setTickCallback(r, t, () => {
var nv = !(<any> r).selected;
setCheckboxValue(r, nv);
if (!!onchg) onchg(nv);
});
if (v !== undefined)
setCheckboxValue(r, v)
return r;
}
export interface RadioGroup
{
buttons:HTMLElement[];
onchange:(n:number)=>void;
change:(n:number)=>void;
current:number;
elt:HTMLElement;
enabled:boolean;
}
export interface RadioItem
{
name: string;
tick?: Ticks;
}
export function mkRadioButtons(lbls:RadioItem[])
{
var res:RadioGroup = {
current: -1,
enabled: true,
buttons:
lbls.map((l, i) => setTickCallback(mkButtonElt("radio-button",
div("radio-outer", div("radio-inner")), div("radio-label", l.name)), l.tick, () => {
if (res.enabled)
res.change(i)
})),
onchange: (n) => {},
change: (n) => {
res.current = n;
res.buttons.forEach((b, i) => b.setFlag("selected", i == n))
res.onchange(n);
},
elt: div("radio-group")
}
res.elt.setChildren(res.buttons)
return res
}
export function mkModalList(children:any[])
{
var kindList = div("modalList", children);
Util.setupDragToScroll(kindList);
return kindList;
}
var progressNotificationAnimation: Animation;
export var showProgressNotification = (msgText:string, fadeOut:boolean = true, delay : number = 1000, duration : number = 2000) =>
{
if (Browser.isHeadless) {
Util.log("progress: " + msgText);
return;
}
var className = "progressNotification";
var se = elt("root");
var oldMsgs = se.getElementsByClassName(className);
var msg = oldMsgs.length > 0 ? <HTMLElement>oldMsgs.item(0) : undefined;
var f = function() {
progressNotificationAnimation = undefined;
if (fadeOut) {
progressNotificationAnimation = Animation.fadeOut(msg);
progressNotificationAnimation.delay = delay;
progressNotificationAnimation.duration = duration;
progressNotificationAnimation.completed = () => { progressNotificationAnimation = undefined; }
progressNotificationAnimation.begin();
}
};
if (msg) {
if (!!progressNotificationAnimation) {
progressNotificationAnimation.stop();
}
msg.style.opacity = "1";
if (msgText !== undefined) {
msg.removeAllChildren();
msg.appendChildren(msgText);
}
f();
} else if (msgText) {
msg = div(className, msgText);
se.appendChild(msg);
progressNotificationAnimation = Animation.fadeIn(msg);
progressNotificationAnimation.completed = f;
progressNotificationAnimation.begin();
}
}
export function showWarningNotification(msgText: string, details: string = null) {
if (Browser.isHeadless) {
Util.log("warning: " + msgText);
return;
}
var msg = div("warningNotification",
div('frownie', ":("), div('info', msgText)
);
if (details) {
msg.appendChild(div('info link', 'learn more...'));
msg.withClick(() => {
tick(Ticks.warningNotificationTap);
ModalDialog.info(msgText, details);
});
}
elt("root").appendChild(msg);
var a = Animation.fadeOut(msg);
a.delay = 6000;
a.duration = 3000;
a.begin();
}
export function showPluginNotification(msgText: string) {
var msg = div("pluginNotification", div('info', msgText));
elt("root").appendChild(msg);
var a = Animation.fadeOut(msg);
a.delay = 6000;
a.duration = 3000;
a.begin();
}
export function showUndoNotification(msgText: string, undo: () => void) {
var previous = elt("infoNotification"); if (previous) previous.removeSelf();
var msg = divId("infoNotification", "infoNotification", msgText, HTML.mkButtonOnce(lf("undo"),() => {
msg.removeSelf();
undo();
}));
elt("root").appendChild(msg);
var fi = Animation.fadeIn(msg);
fi.completed = () => {
var a = Animation.fadeOut(msg);
a.delay = 4000;
a.duration = 1000;
a.begin();
}
fi.begin();
}
export function showErrorNotification(msgText:string)
{
if (Browser.isHeadless) {
Util.log("error: " + msgText);
return;
}
var msg = div("errorNotification", msgText);
elt("root").appendChild(msg);
var a = Animation.fadeOut(msg);
a.delay = 2000;
a.duration = 2000;
a.begin();
}
export function showSaveNotification(msgText:string, time = 1000)
{
var msg = div("saveNotification", msgText);
elt("root").appendChild(msg);
var a = Animation.fadeOut(msg);
a.delay = time;
a.duration = 1000;
a.begin();
return msg
}
export interface NotificationOptions {
lang?: string;
body?: string;
tag?: string;
icon?: string;
};
export function showWebNotification(aTitle: string, aOptions: NotificationOptions = {}, aTimeout=10000) {
if (!("Notification" in window))
return;
if (document.hasFocus())
return;
var Notification = (<any>window).Notification;
var doit = () => {
var n = new Notification(aTitle, aOptions);
n.onshow = () => {
Util.setTimeout(aTimeout, () => n.close());
}
};
if (Notification.permission === "granted") {
doit();
} else {
Notification.requestPermission(function (permission) {
if (permission === "granted")
doit();
});
}
}
export function mkA(cl:string, href:string, target:string, ...children:any[]):HTMLAnchorElement
{
var elt = <HTMLAnchorElement>document.createElement("a");
elt.href = href;
elt.target = target;
if (cl)
elt.className = cl;
elt.appendChildren(children);
return elt;
}
export function span(cls:string, ...elts:any[]):HTMLElement
{
var r = document.createElement("span")
if (cls)
r.className = cls;
r.setChildren(elts)
return r;
}
export function label(cls:string, ...elts:any[]):HTMLElement
{
var r = document.createElement("label")
if (cls)
r.className = cls;
r.setChildren(elts)
return r;
}
export function showNotification(msg: HTMLElement) {
elt("root").appendChild(msg);
var a = Animation.fadeOut(msg);
a.delay = 6000;
a.duration = 2000;
a.begin();
}
export function showNotificationText(text: string) {
var msg = div("errorNotification", text);
showNotification(msg);
}
export function showProxyNotification(message: string, url: string)
{
var msg = div("errorNotification",
message, mkBr(), span("smallText", "URL: " + url));
showNotification(msg);
}
export function showCorsNotification(url: string)
{
var msg = div("errorNotification",
lf("Access Denied: Your web browser and the web site prevent cross-origin resource sharing (CORS)."),
mkA("", Cloud.config.rootUrl + "/docs/CORS", "_blank", "Learn more..."), mkBr(), span("smallText", "URL: " + url));
showNotification(msg);
}
export interface ProgressBar
extends HTMLElement
{
start():void;
stop():void;
reset():void;
}
export function mkProgressBar():ProgressBar
{
var r = <ProgressBar>div("progressBar", Util.range(0, 4).map((v) => div("progressDot progressDot-" + v)));
HTML.setRole(r, "progressbar");
var n = 0;
function update(k: number) {
n += k;
if (n < 0) n = 0;
r.style.display = n > 0 ? "block" : "none";
}
update(0);
if (Browser.noAnimations) {
r.start = r.stop = r.reset = () => { };
} else {
r.start = () => { update(+1) };
r.stop = () => { update(-1) };
r.reset = () => { update(-n) };
}
return r;
}
export interface AutoExpandingTextAreaOptions {
showDismiss?: boolean;
editFullScreenAsync?: (text: string) => Promise; // string
}
export interface AutoExpandingTextArea {
div: HTMLElement;
textarea: HTMLTextAreaElement;
update: () => void;
onUpdate: () => void;
dismiss: HTMLElement;
onDismiss: () => void;
fullScreen: HTMLElement;
}
export function mkAutoExpandingTextArea(options: AutoExpandingTextAreaOptions = {}): AutoExpandingTextArea
{
var ta = HTML.mkTextArea();
var pre = document.createElement("pre");
var dismiss: HTMLElement;
var fullScreen: HTMLElement;
var btns: HTMLElement;
if (options.showDismiss || options.editFullScreenAsync) {
btns = div('close-round-buttons');
if (options.showDismiss)
btns.appendChild(dismiss = div('',HTML.mkImg("svg:check,black")).withClick(() => {
if (r.onDismiss) r.onDismiss();
}));
if (options.editFullScreenAsync)
btns.appendChild(fullScreen = div('',HTML.mkImg('svg:expand,black')).withClick(() => {
options.editFullScreenAsync(ta.value).done(value => {
ta.value = value;
if (r.onDismiss) r.onDismiss();
})
}));
}
var content = span(null, null)
pre.setChildren([content, mkBr()])
var update = () => {
content.textContent = ta.value;
r.onUpdate();
}
Util.onInputChange(ta, update)
var r = {
div: div("expandingTextAreaContainer", pre, ta, btns),
textarea: ta,
update: update,
onUpdate: () => {},
dismiss: dismiss,
onDismiss: () => { },
fullScreen: fullScreen,
}
return r;
}
export function fixWp8Links(...elts:HTMLElement[])
{
// if (!Browser.isWP8app) return;
elts.forEach((elt) => {
var ch = elt.getElementsByTagName("A");
for (var i = 0; i < ch.length; ++i) (() => {
var a = <HTMLAnchorElement>ch[i];
var href = a.getAttribute("href");
if (/^#/.test(href)) {
a.withClick(() => {
Util.log("navigate " + href);
Util.setHash(href)
return false;
})
}
})()
})
}
export var localCdn:string = null;
export function proxyResource(url:string)
{
// Must be idempotent
if (!url) return url;
// only do it for az31353.vo.msecnd.net ?
if (localCdn && !/http:\/\/localhost/i.test(url) &&
/^(https:\/\/az31353.vo.msecnd.net|http:\/\/cdn.touchdevelop.com|https?:\/\/lexmediaservice3.blob.core.windows.net|https:\/\/tdtutorialtranslator.blob.core.windows.net)/i.test(url)) {
url = localCdn + encodeURIComponent(url)
}
return url
}
export function cssImage(url:string, opacity = 1) : string
{
if (!url) return "";
var u = "url(" + proxyResource(url) + ")";
if (opacity <= 1)
u = Util.fmt("linear-gradient(to bottom, rgba(255,255,255,{0}) 0%,rgba(255,255,255,{0}) 100%), {1}", (1-opacity).toFixed(3) , u);
return u;
}
export var html5Tags:any = {
// Forbidden
"dialog": -1, // A dialog box or window
"embed": -1, // A container for an external (non-HTML) application
"keygen": -1, // A key-pair generator field (for forms)
"link": -1, // The relationship between a document and an external resource (most used to link to style sheets)
"meta": -1, // Metadata about an HTML document
"noscript": -1, // An alternate content for users that do not support client-side scripts
"object": -1, // An embedded object
"param": -1, // A parameter for an object
"script": -1, // A client-side script
"applet": -1, // Not supported in HTML5. Use <object> instead.
"frame": -1, // Not supported in HTML5.
"frameset": -1, // Not supported in HTML5.
"noframes": -1, // Not supported in HTML5.
"html": -1, // The root of an HTML document
"body": -1, // The document's body
"head": -1, // Information about the document
"title": -1, // A title for the document
"form": -1, // An HTML form for user input
"style": -1, // Style information for a document
// Not supported.
"basefont": -2, // Not supported in HTML5. Use CSS instead.
"font": -2, // Not supported in HTML5. Use CSS instead.
"center": -2, // Not supported in HTML5. Use CSS instead.
"big": -2, // Not supported in HTML5. Use CSS instead.
"dir": -2, // Not supported in HTML5. Use <ul> instead.
"acronym": -2, // Not supported in HTML5. Use <abbr> instead.
"strike": -2, // Not supported in HTML5. Use <del> instead.
"tt": -2, // Not supported in HTML5. Use CSS instead.
// Supported in and outside markdown
"a": 1, // A hyperlink
"ul": 1, // An unordered list
"h1": 1, // Header 1
"h2": 1, // Header 2
"h3": 1, // Header 3
"h4": 1, // Header 4
"h5": 1, // Header 5
"h6": 1, // Header 6
"ol": 1, // An ordered list
"li": 1, // A list item
"blockquote": 1, // A section that is quoted from another source
"pre": 1, // Preformatted text
"b": 1, // Bold text
"button": 1, // A clickable button
"code": 1, // A piece of computer code
"img": 1, // An image
"strong": 1, // Important text
"span": 1, // A section in a document
"br": 1, // A single line break
"del": 1, // Text that has been deleted from a document
"div": 1, // A section in a document
"em": 1, // Emphasized text
"p": 1, // A paragraph
"i": 1, // A part of text in an alternate voice or mood
"u": 1, // Text that should be stylistically different from normal text
"video": 1, // A video or movie
"source": 1, // Multiple media resources for media elements (<video> and <audio>)
"audio": 1, // Sound content
"track": 1, // Text tracks for media elements (<video> and <audio>)
"small": 1, // Smaller text (supported by bootstrap)
"iframe": 1, // An inline frame
// SVG
"svg": 1,
"path": 1,
"circle": 1,
"g": 1,
// Supported outside markdown
"abbr": 2, // An abbreviation
"address": 2, // Contact information for the author/owner of a document
"area": 2, // An area inside an image-map
"article": 2, // An article
"aside": 2, // Content aside from the page content
"bdi": 2, // Isolates a part of text that might be formatted in a different direction from other text outside it
"bdo": 2, // Overrides the current text direction
"canvas": 2, // Used to draw graphics, on the fly, via scripting (usually JavaScript)
"caption": 2, // A table caption
"cite": 2, // The title of a work
"col": 2, // Specifies column properties for each column within a <colgroup> element
"colgroup": 2, // Specifies a group of one or more columns in a table for formatting
"datalist": 2, // Specifies a list of pre-defined options for input controls
"dd": 2, // A description/value of a term in a description list
"details": 2, // Additional details that the user can view or hide
"dfn": 2, // Represents the defining instance of a term
"dl": 2, // A description list
"dt": 2, // A term/name in a description list
"fieldset": 2, // Groups related elements in a form
"figcaption": 2, // A caption for a <figure> element
"figure": 2, // Specifies self-contained content
"footer": 2, // A footer for a document or section
"header": 2, // A header for a document or section
"hgroup": 2, // A group of headings
"hr": 2, // A thematic change in the content
"input": 2, // An input control
"ins": 2, // A text that has been inserted into a document
"kbd": 2, // Keyboard input
"label": 2, // A label for an <input> element
"legend": 2, // A caption for a <fieldset> element
"main": 2, // Specifies the main content of a document
"map": 2, // A client-side image-map
"mark": 2, // Marked/highlighted text
"menu": 2, // A list/menu of commands
"menuitem": 2, // A command/menu item that the user can invoke from a popup menu
"meter": 2, // A scalar measurement within a known range (a gauge)
"nav": 2, // Navigation links
"optgroup": 2, // A group of related options in a drop-down list
"option": 2, // An option in a drop-down list
"output": 2, // The result of a calculation
"progress": 2, // Represents the progress of a task
"q": 2, // A short quotation
"rp": 2, // What to show in browsers that do not support ruby annotations
"rt": 2, // An explanation/pronunciation of characters (for East Asian typography)
"ruby": 2, // A ruby annotation (for East Asian typography)
"s": 2, // Text that is no longer correct
"samp": 2, // Sample output from a computer program
"section": 2, // A section in a document
"select": 2, // A drop-down list
"sub": 2, // Subscripted text
"summary": 2, // A visible heading for a <details> element
"sup": 2, // Superscripted text
"table": 2, // A table
"tbody": 2, // Groups the body content in a table
"td": 2, // A cell in a table
"textarea": 2, // A multiline input control (text area)
"tfoot": 2, // Groups the footer content in a table
"th": 2, // A header cell in a table
"thead": 2, // Groups the header content in a table
"time": 2, // A date/time
"tr": 2, // A row in a table
"var": 2, // A variable
"wbr": 2, // A possible line-break
}
var html5Attributes = {
// URL
"src": 1,
"srcset": 1,
"href": 1,
"xmlns": 1, //SVG
// non-URL
"class": 2,
"frameborder": 2,
"allowfullscreen": 2,
"alt": 2,
"style": 2,
"type":2,
"target": 2,
"rel": 2,
"name": 2,
"translate": 2,
"dir": 2,
"id": 2,
"width": 2,
"height": 2,
"placeholder": 2,
"title":2,
// video
"controls": 2,
"autoplay": 2,
"disabled": 2,
// accessibility,
"role":2,
"aria-atomic": 2,
"aria-busy": 2,
"aria-controls": 2,
"aria-describedby": 2,
"aria-disabled": 2,
"aria-dropeffect": 2,
"aria-flowto": 2,
"aria-grabbed": 2,
"aria-haspopup": 2,
"aria-hidden": 2,
"aria-invalid": 2,
"aria-label": 2,
"aria-labelledby": 2,
"aria-live": 2,
"aria-owns": 2,
"aria-relevant": 2,
// svg
"viewbox": 2,
"preserveaspectratio": 2,
"fill": 2,
"d": 2,
"cx": 2,
"cy": 2,
"r": 2,
"stroke": 2,
"stroke-width": 2,
"transform": 2,
"fill-opacity": 2,
"stroke-miterlimit": 2,
"stroke-dasharray": 2,
}
function htmlOops(msg:string, html:string, other?:string)
{
//console.log("HTML: " + html)
//console.log("HTML: " + msg + ": " + other)
if (other) msg += ": " + other.slice(0, 100)
var err:any = new Error("Critical: HTML sanitization failure, " + msg)
err.bugAttachments = [html]
if (other && other.length > 100) err.bugAttachments.push(other)
throw err
}
function validateTag(t:string, html:string) {
if (/^\!--.*--$/.exec(t))
return
var m = /^\/?([a-zA-Z0-9]+)(\s|\/?$)/.exec(t)
if (!m) htmlOops("no tag name", html, t)
var tn = m[1].toLowerCase()
if (!html5Tags.hasOwnProperty(tn))
htmlOops("unknown tag", html, tn)
var v = html5Tags[tn]
if (v !== 1)
htmlOops("tag not allowed, " + v, html, tn)
t = t.slice(m[0].length)
while (!/^\s*\/?$/.test(t)) {
m = /^\s*([a-zA-Z0-9-]+)($|\s|="([^"]*)"|='([^']*)'|=([a-zA-Z0-9]+))/.exec(t)
if (!m) htmlOops("cannot parse html attribute", html, t)
var an = m[1].toLowerCase()
var av = m[3] || m[4] || m[5] || ""
if (/^data-/.test(an)) {}
else if (!html5Attributes.hasOwnProperty(an))
htmlOops("unknown attribute", html, t)
else {
var kk = html5Attributes[an]
if (kk == 1) {
if (!/^(http|\/|\.\/|#|mailto:)/i.test(av))
htmlOops("bad URL", html, t)
} else if (kk == 2) {
} else {
htmlOops("forbidden attribute", html, t)
}
}
t = t.slice(m[0].length)
}
}
export function sanitizeHTML(html:string) : string
{
if (!isBeta) return html
if (!html) return html;
try {
var reminder = html.replace(/<([^<>]+)>/g, (allm, t) => {
validateTag(t, html)
return "(tag)"
})
if (/[<>]/.test(reminder)) {
htmlOops("unexpected tag", html, reminder)
}
return html
} catch (e) {
Util.reportError("html", e, false)
return html
}
}
export function allowedTagName(tn:string)
{
tn = tn.toLowerCase()
if (!html5Tags.hasOwnProperty(tn))
return false
var v = html5Tags[tn]
return v === 1 || v === 2
}
export function allowedAttribute(name: string, val: string) {
if (/^data-/.test(name))
return true
if (!html5Attributes.hasOwnProperty(name))
return false
var v = html5Attributes[val]
if (v === 1)
// essentially, we want to exclude javascript:..., but it can be written in many ways, so we go for a white-list instead
return /^(http|\/|\.\/|#|mailto:)/.test(val)
else if (v === 2)
return true
else
return false
}
}