fune/toolkit/content/aboutwebrtc/aboutWebrtc.js
Andrew McCreight 5dec0e0beb Bug 1432992, part 1 - Remove definitions of Ci, Cr, Cc, and Cu. r=florian
This patch was autogenerated by my decomponents.py

It covers almost every file with the extension js, jsm, html, py,
xhtml, or xul.

It removes blank lines after removed lines, when the removed lines are
preceded by either blank lines or the start of a new block. The "start
of a new block" is defined fairly hackily: either the line starts with
//, ends with */, ends with {, <![CDATA[, """ or '''. The first two
cover comments, the third one covers JS, the fourth covers JS embedded
in XUL, and the final two cover JS embedded in Python. This also
applies if the removed line was the first line of the file.

It covers the pattern matching cases like "var {classes: Cc,
interfaces: Ci, utils: Cu, results: Cr} = Components;". It'll remove
the entire thing if they are all either Ci, Cr, Cc or Cu, or it will
remove the appropriate ones and leave the residue behind. If there's
only one behind, then it will turn it into a normal, non-pattern
matching variable definition. (For instance, "const { classes: Cc,
Constructor: CC, interfaces: Ci, utils: Cu } = Components" becomes
"const CC = Components.Constructor".)

MozReview-Commit-ID: DeSHcClQ7cG

--HG--
extra : rebase_source : d9c41878036c1ef7766ef5e91a7005025bc1d72b
2018-02-06 09:36:57 -08:00

972 lines
27 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.defineModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "FilePicker",
"@mozilla.org/filepicker;1", "nsIFilePicker");
XPCOMUtils.defineLazyGetter(this, "strings", () => {
return Services.strings.createBundle("chrome://global/locale/aboutWebrtc.properties");
});
const getString = strings.GetStringFromName;
const formatString = strings.formatStringFromName;
const LOGFILE_NAME_DEFAULT = "aboutWebrtc.html";
const WEBRTC_TRACE_ALL = 65535;
function getStats() {
return new Promise(resolve =>
WebrtcGlobalInformation.getAllStats(stats => resolve(stats)));
}
function getLog() {
return new Promise(resolve =>
WebrtcGlobalInformation.getLogging("", log => resolve(log)));
}
// Begin initial data queries as page loads. Store returned Promises for
// later use.
var reportsRetrieved = getStats();
var logRetrieved = getLog();
function onLoad() {
document.title = getString("document_title");
let controls = document.querySelector("#controls");
if (controls) {
let set = ControlSet.render();
ControlSet.add(new SavePage());
ControlSet.add(new DebugMode());
ControlSet.add(new AecLogging());
controls.appendChild(set);
}
let contentElem = document.querySelector("#content");
if (!contentElem) {
return;
}
let contentInit = function(data) {
AboutWebRTC.init(onClearStats, onClearLog);
AboutWebRTC.render(contentElem, data);
};
Promise.all([reportsRetrieved, logRetrieved])
.then(([stats, log]) => contentInit({reports: stats.reports, log}))
.catch(error => contentInit({error}));
}
function onClearLog() {
WebrtcGlobalInformation.clearLogging();
getLog()
.then(log => AboutWebRTC.refresh({log}))
.catch(error => AboutWebRTC.refresh({logError: error}));
}
function onClearStats() {
WebrtcGlobalInformation.clearAllStats();
getStats()
.then(stats => AboutWebRTC.refresh({reports: stats.reports}))
.catch(error => AboutWebRTC.refresh({reportError: error}));
}
var ControlSet = {
render() {
let controls = renderElement("div", null, {className: "controls"});
this.controlSection = renderElement("div", null, {className: "control"});
this.messageSection = renderElement("div", null, {className: "message"});
controls.appendChild(this.controlSection);
controls.appendChild(this.messageSection);
return controls;
},
add(controlObj) {
let [controlElem, messageElem] = controlObj.render();
this.controlSection.appendChild(controlElem);
this.messageSection.appendChild(messageElem);
}
};
function Control() {
this._label = null;
this._message = null;
this._messageHeader = null;
}
Control.prototype = {
render() {
let controlElem = document.createElement("button");
let messageElem = document.createElement("p");
this.ctrl = controlElem;
controlElem.onclick = this.onClick.bind(this);
this.msg = messageElem;
this.update();
return [controlElem, messageElem];
},
set label(val) {
return this._labelVal = val || "\xA0";
},
get label() {
return this._labelVal;
},
set message(val) {
return this._messageVal = val;
},
get message() {
return this._messageVal;
},
update() {
this.ctrl.textContent = this._label;
this.msg.textContent = "";
if (this._message) {
this.msg.appendChild(Object.assign(document.createElement("span"), {
className: "info-label",
textContent: `${this._messageHeader}: `,
}));
this.msg.appendChild(document.createTextNode(this._message));
}
},
onClick(event) {
return true;
}
};
function SavePage() {
Control.call(this);
this._messageHeader = getString("save_page_label");
this._label = getString("save_page_label");
}
SavePage.prototype = Object.create(Control.prototype);
SavePage.prototype.constructor = SavePage;
SavePage.prototype.onClick = function() {
let content = document.querySelector("#content");
if (!content)
return;
FoldEffect.expandAll();
FilePicker.init(window, getString("save_page_dialog_title"), FilePicker.modeSave);
FilePicker.defaultString = LOGFILE_NAME_DEFAULT;
FilePicker.open(rv => {
if (rv == FilePicker.returnOK || rv == FilePicker.returnReplace) {
let fout = FileUtils.openAtomicFileOutputStream(
FilePicker.file, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE);
let nodes = content.querySelectorAll(".no-print");
let noPrintList = [];
for (let node of nodes) {
noPrintList.push(node);
node.style.setProperty("display", "none");
}
fout.write(content.outerHTML, content.outerHTML.length);
FileUtils.closeAtomicFileOutputStream(fout);
for (let node of noPrintList) {
node.style.removeProperty("display");
}
this._message = formatString("save_page_msg", [FilePicker.file.path], 1);
this.update();
}
});
};
function DebugMode() {
Control.call(this);
this._messageHeader = getString("debug_mode_msg_label");
if (WebrtcGlobalInformation.debugLevel > 0) {
this.onState();
} else {
this._label = getString("debug_mode_off_state_label");
this._message = null;
}
}
DebugMode.prototype = Object.create(Control.prototype);
DebugMode.prototype.constructor = DebugMode;
DebugMode.prototype.onState = function() {
this._label = getString("debug_mode_on_state_label");
try {
let file = Services.prefs.getCharPref("media.webrtc.debug.log_file");
this._message = formatString("debug_mode_on_state_msg", [file], 1);
} catch (e) {
this._message = null;
}
};
DebugMode.prototype.offState = function() {
this._label = getString("debug_mode_off_state_label");
try {
let file = Services.prefs.getCharPref("media.webrtc.debug.log_file");
this._message = formatString("debug_mode_off_state_msg", [file], 1);
} catch (e) {
this._message = null;
}
};
DebugMode.prototype.onClick = function() {
if (WebrtcGlobalInformation.debugLevel > 0) {
WebrtcGlobalInformation.debugLevel = 0;
this.offState();
} else {
WebrtcGlobalInformation.debugLevel = WEBRTC_TRACE_ALL;
this.onState();
}
this.update();
};
function AecLogging() {
Control.call(this);
this._messageHeader = getString("aec_logging_msg_label");
if (WebrtcGlobalInformation.aecDebug) {
this.onState();
} else {
this._label = getString("aec_logging_off_state_label");
this._message = null;
}
}
AecLogging.prototype = Object.create(Control.prototype);
AecLogging.prototype.constructor = AecLogging;
AecLogging.prototype.offState = function() {
this._label = getString("aec_logging_off_state_label");
try {
let file = WebrtcGlobalInformation.aecDebugLogDir;
this._message = formatString("aec_logging_off_state_msg", [file], 1);
} catch (e) {
this._message = null;
}
};
AecLogging.prototype.onState = function() {
this._label = getString("aec_logging_on_state_label");
try {
this._message = getString("aec_logging_on_state_msg");
} catch (e) {
this._message = null;
}
};
AecLogging.prototype.onClick = function() {
if (WebrtcGlobalInformation.aecDebug) {
WebrtcGlobalInformation.aecDebug = false;
this.offState();
} else {
WebrtcGlobalInformation.aecDebug = true;
this.onState();
}
this.update();
};
var AboutWebRTC = {
_reports: [],
_log: [],
init(onClearStats, onClearLog) {
this._onClearStats = onClearStats;
this._onClearLog = onClearLog;
},
render(parent, data) {
this._content = parent;
this._setData(data);
if (data.error) {
parent.appendChild(renderElement("h3", getString("cannot_retrieve_log")));
parent.appendChild(
renderElement("p", `${data.error.name}: ${data.error.message}`));
return;
}
this._peerConnections = this.renderPeerConnections();
this._connectionLog = this.renderConnectionLog();
this._content.appendChild(this._peerConnections);
this._content.appendChild(this._connectionLog);
},
_setData(data) {
if (data.reports) {
this._reports = data.reports;
}
if (data.log) {
this._log = data.log;
}
},
refresh(data) {
this._setData(data);
let pc = this._peerConnections;
this._peerConnections = this.renderPeerConnections();
let log = this._connectionLog;
this._connectionLog = this.renderConnectionLog();
this._content.replaceChild(this._peerConnections, pc);
this._content.replaceChild(this._connectionLog, log);
},
renderPeerConnections() {
let connections = renderElement("div", null, {className: "stats"});
let heading = renderElement("span", null, {className: "section-heading"});
heading.appendChild(renderElement("h3", getString("stats_heading")));
heading.appendChild(renderElement("button", getString("stats_clear"), {
className: "no-print",
onclick: this._onClearStats
}));
connections.appendChild(heading);
if (!this._reports || !this._reports.length) {
return connections;
}
let reports = [...this._reports];
reports.sort((a, b) => b.timestamp - a.timestamp);
for (let report of reports) {
let peerConnection = new PeerConnection(report);
connections.appendChild(peerConnection.render());
}
return connections;
},
renderConnectionLog() {
let content = renderElement("div", null, {className: "log"});
let heading = renderElement("span", null, {className: "section-heading"});
heading.appendChild(renderElement("h3", getString("log_heading")));
heading.appendChild(renderElement("button", getString("log_clear"), {
className: "no-print",
onclick: this._onClearLog
}));
content.appendChild(heading);
if (!this._log || !this._log.length) {
return content;
}
let div = new FoldableSection(content, {
showMsg: getString("log_show_msg"),
hideMsg: getString("log_hide_msg")
}).render();
for (let line of this._log) {
div.appendChild(renderElement("p", line));
}
content.appendChild(div);
return content;
}
};
function PeerConnection(report) {
this._report = report;
}
PeerConnection.prototype = {
render() {
let pc = renderElement("div", null, {className: "peer-connection"});
pc.appendChild(this.renderHeading());
let div = new FoldableSection(pc).render();
div.appendChild(this.renderDesc());
div.appendChild(new ICEStats(this._report).render());
div.appendChild(new SDPStats(this._report).render());
div.appendChild(new RTPStats(this._report).render());
pc.appendChild(div);
return pc;
},
renderHeading() {
let pcInfo = this.getPCInfo(this._report);
let heading = document.createElement("h3");
let now = new Date(this._report.timestamp).toString();
heading.textContent =
`[ ${pcInfo.id} ] ${pcInfo.url} ${pcInfo.closed ? `(${getString("connection_closed")})` : ""} ${now}`;
return heading;
},
renderDesc() {
let info = document.createElement("div");
info.appendChild(
renderElement("span", `${getString("peer_connection_id_label")}: `), {
className: "info-label"
});
info.appendChild(renderElement("span", this._report.pcid, {
className: "info-body"
}));
return info;
},
getPCInfo(report) {
return {
id: report.pcid.match(/id=(\S+)/)[1],
url: report.pcid.match(/url=([^)]+)/)[1],
closed: report.closed
};
}
};
function renderElement(elemName, elemText, options = {}) {
let elem = document.createElement(elemName);
// check for null instead of using elemText || "" so we don't hide
// elements with 0 values
if (elemText != null) {
elem.textContent = elemText;
}
Object.assign(elem, options);
return elem;
}
function SDPStats(report) {
this._report = report;
}
SDPStats.prototype = {
render() {
let div = document.createElement("div");
div.appendChild(renderElement("h4", getString("sdp_heading")));
let offerLabel = `(${getString("offer")})`;
let answerLabel = `(${getString("answer")})`;
let localSdpHeading =
`${getString("local_sdp_heading")} ${this._report.offerer ? offerLabel : answerLabel}`;
let remoteSdpHeading =
`${getString("remote_sdp_heading")} ${this._report.offerer ? answerLabel : offerLabel}`;
div.appendChild(renderElement("h5", localSdpHeading));
div.appendChild(renderElement("pre", this._report.localSdp));
div.appendChild(renderElement("h5", remoteSdpHeading));
div.appendChild(renderElement("pre", this._report.remoteSdp));
return div;
}
};
function RTPStats(report) {
this._report = report;
this._stats = [];
}
RTPStats.prototype = {
render() {
let div = document.createElement("div");
div.appendChild(renderElement("h4", getString("rtp_stats_heading")));
this.generateRTPStats();
for (let statSet of this._stats) {
div.appendChild(this.renderRTPStatSet(statSet));
}
return div;
},
generateRTPStats() {
let remoteRtpStats = {};
let rtpStats = [].concat((this._report.inboundRTPStreamStats || []),
(this._report.outboundRTPStreamStats || []));
// Generate an id-to-streamStat index for each streamStat that is marked
// as a remote. This will be used next to link the remote to its local side.
for (let stats of rtpStats) {
if (stats.isRemote) {
remoteRtpStats[stats.id] = stats;
}
}
// If a streamStat has a remoteId attribute, create a remoteRtpStats
// attribute that references the remote streamStat entry directly.
// That is, the index generated above is merged into the returned list.
for (let stats of rtpStats) {
if (stats.remoteId) {
stats.remoteRtpStats = remoteRtpStats[stats.remoteId];
}
}
this._stats = rtpStats;
},
renderAvStats(stats) {
let statsString = "";
if (stats.mozAvSyncDelay) {
statsString += `${getString("av_sync_label")}: ${stats.mozAvSyncDelay} ms `;
}
if (stats.mozJitterBufferDelay) {
statsString += `${getString("jitter_buffer_delay_label")}: ${stats.mozJitterBufferDelay} ms`;
}
return renderElement("p", statsString);
},
renderCoderStats(stats) {
let statsString = "";
let label;
if (stats.bitrateMean) {
statsString += ` ${getString("avg_bitrate_label")}: ${(stats.bitrateMean / 1000000).toFixed(2)} Mbps`;
if (stats.bitrateStdDev) {
statsString += ` (${(stats.bitrateStdDev / 1000000).toFixed(2)} SD)`;
}
}
if (stats.framerateMean) {
statsString += ` ${getString("avg_framerate_label")}: ${(stats.framerateMean).toFixed(2)} fps`;
if (stats.framerateStdDev) {
statsString += ` (${stats.framerateStdDev.toFixed(2)} SD)`;
}
}
if (stats.droppedFrames) {
statsString += ` ${getString("dropped_frames_label")}: ${stats.droppedFrames}`;
}
if (stats.discardedPackets) {
statsString += ` ${getString("discarded_packets_label")}: ${stats.discardedPackets}`;
}
if (statsString) {
label = (stats.packetsReceived ? ` ${getString("decoder_label")}:` : ` ${getString("encoder_label")}:`);
statsString = label + statsString;
}
return renderElement("p", statsString);
},
renderTransportStats(stats, typeLabel) {
let time = new Date(stats.timestamp).toTimeString();
let statsString = `${typeLabel}: ${time} ${stats.type} SSRC: ${stats.ssrc}`;
if (stats.packetsReceived) {
statsString += ` ${getString("received_label")}: ${stats.packetsReceived} ${getString("packets")}`;
if (stats.bytesReceived) {
statsString += ` (${(stats.bytesReceived / 1024).toFixed(2)} Kb)`;
}
statsString += ` ${getString("lost_label")}: ${stats.packetsLost} ${getString("jitter_label")}: ${stats.jitter}`;
if (stats.roundTripTime) {
statsString += ` RTT: ${stats.roundTripTime} ms`;
}
} else if (stats.packetsSent) {
statsString += ` ${getString("sent_label")}: ${stats.packetsSent} ${getString("packets")}`;
if (stats.bytesSent) {
statsString += ` (${(stats.bytesSent / 1024).toFixed(2)} Kb)`;
}
}
return renderElement("p", statsString);
},
renderRTPStatSet(stats) {
let div = document.createElement("div");
div.appendChild(renderElement("h5", stats.id));
if (stats.MozAvSyncDelay || stats.mozJitterBufferDelay) {
div.appendChild(this.renderAvStats(stats));
}
div.appendChild(this.renderCoderStats(stats));
div.appendChild(this.renderTransportStats(stats, getString("typeLocal")));
if (stats.remoteId && stats.remoteRtpStats) {
div.appendChild(this.renderTransportStats(stats.remoteRtpStats, getString("typeRemote")));
}
return div;
},
};
function ICEStats(report) {
this._report = report;
}
ICEStats.prototype = {
render() {
let div = document.createElement("div");
div.appendChild(renderElement("h4", getString("ice_stats_heading")));
div.appendChild(this.renderICECandidateTable());
// add just a bit of vertical space between the restart/rollback
// counts and the ICE candidate pair table above.
div.appendChild(document.createElement("br"));
div.appendChild(this.renderIceMetric("ice_restart_count_label",
this._report.iceRestarts));
div.appendChild(this.renderIceMetric("ice_rollback_count_label",
this._report.iceRollbacks));
div.appendChild(this.renderRawICECandidateSection());
return div;
},
renderICECandidateTable() {
let caption = renderElement("caption", null, {className: "no-print"});
// This takes the caption message with the replacement token, breaks
// it around the token, and builds the spans for each portion of the
// caption. This is to allow localization to put the color name for
// the highlight wherever it is appropriate in the translated string
// while avoiding innerHTML warnings from eslint.
let captionTemplate = getString("trickle_caption_msg2");
let [start, end] = captionTemplate.split(/%(?:1\$)?S/);
// only append span if non-whitespace chars present
if (/\S/.test(start)) {
caption.appendChild(
renderElement("span", `${start}`));
}
caption.appendChild(
renderElement("span", getString("trickle_highlight_color_name2"), {
className: "trickled"
}));
// only append span if non-whitespace chars present
if (/\S/.test(end)) {
caption.appendChild(
renderElement("span", `${end}`));
}
let stats = this.generateICEStats();
// don't use |stat.x || ""| here because it hides 0 values
let tbody = stats.map(stat => [
stat["local-candidate"],
stat["remote-candidate"],
stat.componentId,
stat.state,
stat.priority,
stat.nominated,
stat.selected,
stat.bytesSent,
stat.bytesReceived
].map(entry => Object.is(entry, undefined) ? "" : entry));
let statsTable = new SimpleTable(
["local_candidate", "remote_candidate",
"ice_component_id", "ice_state",
"priority", "nominated", "selected",
"ice_pair_bytes_sent", "ice_pair_bytes_received"
].map(columnName => getString(columnName)),
tbody, caption).render();
// after rendering the table, we need to change the class name for each
// candidate pair's local or remote candidate if it was trickled.
stats.forEach((stat, index) => {
// look at statsTable row index + 1 to skip column headers
let rowIndex = index + 1;
if (stat["remote-trickled"]) {
statsTable.rows[rowIndex].cells[1].className = "trickled";
}
if (stat["local-trickled"]) {
statsTable.rows[rowIndex].cells[0].className = "trickled";
}
});
// if the next row's component id changes, mark the bottom of the
// current row with a thin, black border to differentiate the
// component id grouping.
let rowCount = statsTable.rows.length - 1;
for (var i = 0; i < rowCount; i++) {
if (statsTable.rows[i].cells[2].innerHTML !==
statsTable.rows[i + 1].cells[2].innerHTML) {
statsTable.rows[i].className = "bottom-border";
}
}
return statsTable;
},
renderRawICECandidates() {
let div = document.createElement("div");
let tbody = [];
let rows = this.generateRawICECandidates();
for (let row of rows) {
tbody.push([row.local, row.remote]);
}
let statsTable = new SimpleTable(
[getString("raw_local_candidate"), getString("raw_remote_candidate")],
tbody).render();
// we want different formatting on the raw stats table (namely, left-align)
statsTable.className = "raw-candidate";
div.appendChild(statsTable);
return div;
},
renderRawICECandidateSection() {
let section = document.createElement("div");
section.appendChild(
renderElement("h4", getString("raw_candidates_heading")));
let div = new FoldableSection(section, {
showMsg: getString("raw_cand_show_msg"),
hideMsg: getString("raw_cand_hide_msg")
}).render();
div.appendChild(this.renderRawICECandidates());
section.appendChild(div);
return section;
},
generateRawICECandidates() {
let rows = [];
let row;
let rawLocals = this._report.rawLocalCandidates.sort();
// add to a Set (to remove duplicates) because some of these come from
// candidates in use and some come from the raw trickled candidates
// received that may have been dropped because no stream was found or
// they were for a component id that was too high.
let rawRemotes = [...new Set(this._report.rawRemoteCandidates)].sort();
let rowCount = Math.max(rawLocals.length, rawRemotes.length);
for (var i = 0; i < rowCount; i++) {
let rawLocal = rawLocals[i];
let rawRemote = rawRemotes[i];
row = {
local: rawLocal || "",
remote: rawRemote || ""
};
rows.push(row);
}
return rows;
},
renderIceMetric(labelName, value) {
let info = document.createElement("div");
info.appendChild(
renderElement("span", `${getString(labelName)}: `, {
className: "info-label"
}));
info.appendChild(
renderElement("span", value, {className: "info-body"}));
return info;
},
generateICEStats() {
// Create an index based on candidate ID for each element in the
// iceCandidateStats array.
let candidates = new Map();
for (let candidate of this._report.iceCandidateStats) {
candidates.set(candidate.id, candidate);
}
// a method to see if a given candidate id is in the array of tickled
// candidates.
let isTrickled = id => [...this._report.trickledIceCandidateStats].some(
candidate => candidate.id == id);
// A component may have a remote or local candidate address or both.
// Combine those with both; these will be the peer candidates.
let matched = {};
let stats = [];
let stat;
for (let pair of this._report.iceCandidatePairStats) {
let local = candidates.get(pair.localCandidateId);
let remote = candidates.get(pair.remoteCandidateId);
if (local) {
stat = {
["local-candidate"]: this.candidateToString(local),
componentId: pair.componentId,
state: pair.state,
priority: pair.priority,
nominated: pair.nominated,
selected: pair.selected,
bytesSent: pair.bytesSent,
bytesReceived: pair.bytesReceived
};
matched[local.id] = true;
if (isTrickled(local.id)) {
stat["local-trickled"] = true;
}
if (remote) {
stat["remote-candidate"] = this.candidateToString(remote);
matched[remote.id] = true;
if (isTrickled(remote.id)) {
stat["remote-trickled"] = true;
}
}
stats.push(stat);
}
}
// sort (group by) componentId first, then bytesSent if available, else by
// priority
return stats.sort((a, b) => {
if (a.componentId != b.componentId) {
return a.componentId - b.componentId;
}
return (b.bytesSent ?
(b.bytesSent || 0) - (a.bytesSent || 0) :
(b.priority || 0) - (a.priority || 0)
);
});
},
candidateToString(c) {
if (!c) {
return "*";
}
var type = c.candidateType;
if (c.type == "local-candidate" && c.candidateType == "relayed") {
type = `${c.candidateType}-${c.mozLocalTransport}`;
}
return `${c.ipAddress}:${c.portNumber}/${c.transport}(${type})`;
}
};
function FoldableSection(parentElement, options = {}) {
this._foldableElement = document.createElement("div");
if (parentElement) {
let sectionCtrl = renderElement("div", null, {
className: "section-ctrl no-print"
});
let foldEffect = new FoldEffect(this._foldableElement, options);
sectionCtrl.appendChild(foldEffect.render());
parentElement.appendChild(sectionCtrl);
}
}
FoldableSection.prototype = {
render() {
return this._foldableElement;
}
};
function SimpleTable(heading, data, caption) {
this._heading = heading || [];
this._data = data;
this._caption = caption;
}
SimpleTable.prototype = {
renderRow(list, header) {
let row = document.createElement("tr");
let elemType = (header ? "th" : "td");
for (let elem of list) {
row.appendChild(renderElement(elemType, elem));
}
return row;
},
render() {
let table = document.createElement("table");
if (this._caption) {
table.appendChild(this._caption);
}
if (this._heading) {
table.appendChild(this.renderRow(this._heading, true));
}
for (let row of this._data) {
table.appendChild(this.renderRow(row));
}
return table;
}
};
function FoldEffect(targetElem, options = {}) {
if (targetElem) {
this._showMsg = "\u25BC " + (options.showMsg || getString("fold_show_msg"));
this._showHint = options.showHint || getString("fold_show_hint");
this._hideMsg = "\u25B2 " + (options.hideMsg || getString("fold_hide_msg"));
this._hideHint = options.hideHint || getString("fold_hide_hint");
this._target = targetElem;
}
}
FoldEffect.prototype = {
render() {
this._target.classList.add("fold-target");
let ctrl = renderElement("div", null, {className: "fold-trigger"});
this._trigger = ctrl;
ctrl.addEventListener("click", this.onClick.bind(this));
this.close();
FoldEffect._sections.push(this);
return ctrl;
},
onClick() {
if (this._target.classList.contains("fold-closed")) {
this.open();
} else {
this.close();
}
return true;
},
open() {
this._target.classList.remove("fold-closed");
this._trigger.setAttribute("title", this._hideHint);
this._trigger.textContent = this._hideMsg;
},
close() {
this._target.classList.add("fold-closed");
this._trigger.setAttribute("title", this._showHint);
this._trigger.textContent = this._showMsg;
}
};
FoldEffect._sections = [];
FoldEffect.expandAll = function() {
for (let section of this._sections) {
section.open();
}
};
FoldEffect.collapseAll = function() {
for (let section of this._sections) {
section.close();
}
};