summaryrefslogtreecommitdiffstats
path: root/toolkit/content/aboutwebrtc/aboutWebrtc.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/content/aboutwebrtc/aboutWebrtc.js')
-rw-r--r--toolkit/content/aboutwebrtc/aboutWebrtc.js841
1 files changed, 841 insertions, 0 deletions
diff --git a/toolkit/content/aboutwebrtc/aboutWebrtc.js b/toolkit/content/aboutwebrtc/aboutWebrtc.js
new file mode 100644
index 000000000..5c366eeb5
--- /dev/null
+++ b/toolkit/content/aboutwebrtc/aboutWebrtc.js
@@ -0,0 +1,841 @@
+/* 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";
+
+/* global WebrtcGlobalInformation, document */
+
+var Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(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: log}))
+ .catch(error => contentInit({error: error}));
+}
+
+function onClearLog() {
+ WebrtcGlobalInformation.clearLogging();
+ getLog()
+ .then(log => AboutWebRTC.refresh({log: 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: function() {
+ let controls = document.createElement("div");
+ let control = document.createElement("div");
+ let message = document.createElement("div");
+
+ controls.className = "controls";
+ control.className = "control";
+ message.className = "message";
+ controls.appendChild(control);
+ controls.appendChild(message);
+
+ this.controlSection = control;
+ this.messageSection = message;
+ return controls;
+ },
+
+ add: function(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: function () {
+ 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: function() {
+ this.ctrl.textContent = this._label;
+
+ if (this._message) {
+ this.msg.innerHTML =
+ `<span class="info-label">${this._messageHeader}:</span> ${this._message}`;
+ } else {
+ this.msg.innerHTML = null;
+ }
+ },
+
+ onClick: function(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;
+ let rv = FilePicker.show();
+
+ 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 = Services.prefs.getCharPref("media.webrtc.debug.aec_log_dir");
+ 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 {
+ let file = Services.prefs.getCharPref("media.webrtc.debug.aec_log_dir");
+ 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: function(onClearStats, onClearLog) {
+ this._onClearStats = onClearStats;
+ this._onClearLog = onClearLog;
+ },
+
+ render: function(parent, data) {
+ this._content = parent;
+ this._setData(data);
+
+ if (data.error) {
+ let msg = document.createElement("h3");
+ msg.textContent = getString("cannot_retrieve_log");
+ parent.appendChild(msg);
+ msg = document.createElement("p");
+ msg.innerHTML = `${data.error.name}: ${data.error.message}`;
+ parent.appendChild(msg);
+ return;
+ }
+
+ this._peerConnections = this.renderPeerConnections();
+ this._connectionLog = this.renderConnectionLog();
+ this._content.appendChild(this._peerConnections);
+ this._content.appendChild(this._connectionLog);
+ },
+
+ _setData: function(data) {
+ if (data.reports) {
+ this._reports = data.reports;
+ }
+
+ if (data.log) {
+ this._log = data.log;
+ }
+ },
+
+ refresh: function(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: function() {
+ let connections = document.createElement("div");
+ connections.className = "stats";
+
+ let heading = document.createElement("span");
+ heading.className = "section-heading";
+ let elem = document.createElement("h3");
+ elem.textContent = getString("stats_heading");
+ heading.appendChild(elem);
+
+ elem = document.createElement("button");
+ elem.textContent = "Clear History";
+ elem.className = "no-print";
+ elem.onclick = this._onClearStats;
+ heading.appendChild(elem);
+ 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: function() {
+ let content = document.createElement("div");
+ content.className = "log";
+
+ let heading = document.createElement("span");
+ heading.className = "section-heading";
+ let elem = document.createElement("h3");
+ elem.textContent = getString("log_heading");
+ heading.appendChild(elem);
+ elem = document.createElement("button");
+ elem.textContent = "Clear Log";
+ elem.className = "no-print";
+ elem.onclick = this._onClearLog;
+ heading.appendChild(elem);
+ content.appendChild(heading);
+
+ if (!this._log || !this._log.length) {
+ return content;
+ }
+
+ let div = document.createElement("div");
+ let sectionCtrl = document.createElement("div");
+ sectionCtrl.className = "section-ctrl no-print";
+ let foldEffect = new FoldEffect(div, {
+ showMsg: getString("log_show_msg"),
+ hideMsg: getString("log_hide_msg")
+ });
+ sectionCtrl.appendChild(foldEffect.render());
+ content.appendChild(sectionCtrl);
+
+ for (let line of this._log) {
+ elem = document.createElement("p");
+ elem.textContent = line;
+ div.appendChild(elem);
+ }
+
+ content.appendChild(div);
+ return content;
+ }
+};
+
+function PeerConnection(report) {
+ this._report = report;
+}
+
+PeerConnection.prototype = {
+ render: function() {
+ let pc = document.createElement("div");
+ pc.className = "peer-connection";
+ pc.appendChild(this.renderHeading());
+
+ let div = document.createElement("div");
+ let sectionCtrl = document.createElement("div");
+ sectionCtrl.className = "section-ctrl no-print";
+ let foldEffect = new FoldEffect(div);
+ sectionCtrl.appendChild(foldEffect.render());
+ pc.appendChild(sectionCtrl);
+
+ 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: function () {
+ let pcInfo = this.getPCInfo(this._report);
+ let heading = document.createElement("h3");
+ let now = new Date(this._report.timestamp).toTimeString();
+ heading.textContent =
+ `[ ${pcInfo.id} ] ${pcInfo.url} ${pcInfo.closed ? `(${getString("connection_closed")})` : ""} ${now}`;
+ return heading;
+ },
+
+ renderDesc: function() {
+ let info = document.createElement("div");
+ let label = document.createElement("span");
+ let body = document.createElement("span");
+
+ label.className = "info-label";
+ label.textContent = `${getString("peer_connection_id_label")}: `;
+ info.appendChild(label);
+
+ body.className = "info-body";
+ body.textContent = this._report.pcid;
+ info.appendChild(body);
+
+ return info;
+ },
+
+ getPCInfo: function(report) {
+ return {
+ id: report.pcid.match(/id=(\S+)/)[1],
+ url: report.pcid.match(/url=([^)]+)/)[1],
+ closed: report.closed
+ };
+ }
+};
+
+function SDPStats(report) {
+ this._report = report;
+}
+
+SDPStats.prototype = {
+ render: function() {
+ let div = document.createElement("div");
+ let elem = document.createElement("h4");
+
+ elem.textContent = getString("sdp_heading");
+ div.appendChild(elem);
+
+ elem = document.createElement("h5");
+ elem.textContent = getString("local_sdp_heading");
+ div.appendChild(elem);
+
+ elem = document.createElement("pre");
+ elem.textContent = this._report.localSdp;
+ div.appendChild(elem);
+
+ elem = document.createElement("h5");
+ elem.textContent = getString("remote_sdp_heading");
+ div.appendChild(elem);
+
+ elem = document.createElement("pre");
+ elem.textContent = this._report.remoteSdp;
+ div.appendChild(elem);
+
+ return div;
+ }
+};
+
+function RTPStats(report) {
+ this._report = report;
+ this._stats = [];
+}
+
+RTPStats.prototype = {
+ render: function() {
+ let div = document.createElement("div");
+ let heading = document.createElement("h4");
+
+ heading.textContent = getString("rtp_stats_heading");
+ div.appendChild(heading);
+
+ this.generateRTPStats();
+
+ for (let statSet of this._stats) {
+ div.appendChild(this.renderRTPStatSet(statSet));
+ }
+
+ return div;
+ },
+
+ generateRTPStats: function() {
+ 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: function(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`;
+ }
+
+ let line = document.createElement("p");
+ line.textContent = statsString;
+ return line;
+ },
+
+ renderCoderStats: function(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;
+ }
+
+ let line = document.createElement("p");
+ line.textContent = statsString;
+ return line;
+ },
+
+ renderTransportStats: function(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.mozRtt) {
+ statsString += ` RTT: ${stats.mozRtt} ms`;
+ }
+ } else if (stats.packetsSent) {
+ statsString += ` ${getString("sent_label")}: ${stats.packetsSent} ${getString("packets")}`;
+ if (stats.bytesSent) {
+ statsString += ` (${(stats.bytesSent / 1024).toFixed(2)} Kb)`;
+ }
+ }
+
+ let line = document.createElement("p");
+ line.textContent = statsString;
+ return line;
+ },
+
+ renderRTPStatSet: function(stats) {
+ let div = document.createElement("div");
+ let heading = document.createElement("h5");
+
+ heading.textContent = stats.id;
+ div.appendChild(heading);
+
+ 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: function() {
+ let tbody = [];
+ for (let stat of this.generateICEStats()) {
+ tbody.push([
+ stat.localcandidate || "",
+ stat.remotecandidate || "",
+ stat.state || "",
+ stat.priority || "",
+ stat.nominated || "",
+ stat.selected || ""
+ ]);
+ }
+
+ let statsTable = new SimpleTable(
+ [getString("local_candidate"), getString("remote_candidate"), getString("ice_state"),
+ getString("priority"), getString("nominated"), getString("selected")],
+ tbody);
+
+ let div = document.createElement("div");
+ let heading = document.createElement("h4");
+
+ heading.textContent = getString("ice_stats_heading");
+ div.appendChild(heading);
+ div.appendChild(statsTable.render());
+
+ return div;
+ },
+
+ generateICEStats: function() {
+ // 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 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 = {
+ localcandidate: this.candidateToString(local),
+ state: pair.state,
+ priority: pair.priority,
+ nominated: pair.nominated,
+ selected: pair.selected
+ };
+ matched[local.id] = true;
+
+ if (remote) {
+ stat.remotecandidate = this.candidateToString(remote);
+ matched[remote.id] = true;
+ }
+ stats.push(stat);
+ }
+ }
+
+ for (let c of candidates.values()) {
+ if (matched[c.id])
+ continue;
+
+ stat = {};
+ stat[c.type] = this.candidateToString(c);
+ stats.push(stat);
+ }
+
+ return stats.sort((a, b) => (b.priority || 0) - (a.priority || 0));
+ },
+
+ candidateToString: function(c) {
+ if (!c) {
+ return "*";
+ }
+
+ var type = c.candidateType;
+
+ if (c.type == "localcandidate" && c.candidateType == "relayed") {
+ type = `${c.candidateType}-${c.mozLocalTransport}`;
+ }
+
+ return `${c.ipAddress}:${c.portNumber}/${c.transport}(${type})`;
+ }
+};
+
+function SimpleTable(heading, data) {
+ this._heading = heading || [];
+ this._data = data;
+}
+
+SimpleTable.prototype = {
+ renderRow: function(list) {
+ let row = document.createElement("tr");
+
+ for (let elem of list) {
+ let cell = document.createElement("td");
+ cell.textContent = elem;
+ row.appendChild(cell);
+ }
+
+ return row;
+ },
+
+ render: function() {
+ let table = document.createElement("table");
+
+ if (this._heading) {
+ table.appendChild(this.renderRow(this._heading));
+ }
+
+ 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: function() {
+ this._target.classList.add("fold-target");
+
+ let ctrl = document.createElement("div");
+ this._trigger = ctrl;
+ ctrl.className = "fold-trigger";
+ ctrl.addEventListener("click", this.onClick.bind(this));
+ this.close();
+
+ FoldEffect._sections.push(this);
+ return ctrl;
+ },
+
+ onClick: function() {
+ if (this._target.classList.contains("fold-closed")) {
+ this.open();
+ } else {
+ this.close();
+ }
+ return true;
+ },
+
+ open: function() {
+ this._target.classList.remove("fold-closed");
+ this._trigger.setAttribute("title", this._hideHint);
+ this._trigger.textContent = this._hideMsg;
+ },
+
+ close: function() {
+ 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();
+ }
+};