2020-07-15 01:29:15 +02:00
|
|
|
zrequire("keydown_util");
|
|
|
|
zrequire("components");
|
2018-03-29 15:05:37 +02:00
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const noop = function () {};
|
2018-04-04 16:36:49 +02:00
|
|
|
|
2020-07-16 22:40:18 +02:00
|
|
|
const LEFT_KEY = {which: 37, preventDefault: noop, stopPropagation: noop};
|
|
|
|
const RIGHT_KEY = {which: 39, preventDefault: noop, stopPropagation: noop};
|
2018-03-29 15:05:37 +02:00
|
|
|
|
2020-07-15 01:29:15 +02:00
|
|
|
run_test("basics", () => {
|
2019-11-02 00:06:25 +01:00
|
|
|
let keydown_f;
|
|
|
|
let click_f;
|
|
|
|
const tabs = [];
|
|
|
|
let focused_tab;
|
|
|
|
let callback_args;
|
2018-03-29 15:05:37 +02:00
|
|
|
|
|
|
|
function make_tab(i) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const self = {};
|
2018-03-29 15:05:37 +02:00
|
|
|
|
|
|
|
assert.equal(tabs.length, i);
|
|
|
|
|
|
|
|
self.stub = true;
|
|
|
|
self.class = [];
|
|
|
|
|
|
|
|
self.addClass = function (c) {
|
2020-07-15 01:29:15 +02:00
|
|
|
self.class += " " + c;
|
2019-11-02 00:06:25 +01:00
|
|
|
const tokens = self.class.trim().split(/ +/);
|
2020-07-15 01:29:15 +02:00
|
|
|
self.class = _.uniq(tokens).join(" ");
|
2018-03-29 15:05:37 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
self.removeClass = function (c) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const tokens = self.class.trim().split(/ +/);
|
2020-07-15 01:29:15 +02:00
|
|
|
self.class = _.without(tokens, c).join(" ");
|
2018-03-29 15:05:37 +02:00
|
|
|
};
|
|
|
|
|
2019-05-08 09:26:27 +02:00
|
|
|
self.hasClass = function (c) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const tokens = self.class.trim().split(/ +/);
|
2019-05-08 09:26:27 +02:00
|
|
|
return tokens.includes(c);
|
|
|
|
};
|
|
|
|
|
2018-03-29 15:05:37 +02:00
|
|
|
self.data = function (name) {
|
2020-07-15 01:29:15 +02:00
|
|
|
assert.equal(name, "tab-id");
|
2018-03-29 15:05:37 +02:00
|
|
|
return i;
|
|
|
|
};
|
|
|
|
|
2020-07-20 21:24:26 +02:00
|
|
|
self.trigger = function (type) {
|
|
|
|
if (type === "focus") {
|
|
|
|
focused_tab = i;
|
|
|
|
}
|
2018-03-29 15:05:37 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
tabs.push(self);
|
|
|
|
|
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const ind_tab = (function () {
|
|
|
|
const self = {};
|
2018-03-29 15:05:37 +02:00
|
|
|
|
|
|
|
self.stub = true;
|
|
|
|
|
2020-07-20 21:26:58 +02:00
|
|
|
self.on = function (name, f) {
|
|
|
|
if (name === "click") {
|
|
|
|
click_f = f;
|
|
|
|
} else if (name === "keydown") {
|
|
|
|
keydown_f = f;
|
|
|
|
}
|
2018-03-29 15:05:37 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
self.removeClass = function (c) {
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
for (const tab of tabs) {
|
2018-03-29 15:05:37 +02:00
|
|
|
tab.removeClass(c);
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
}
|
2018-03-29 15:05:37 +02:00
|
|
|
};
|
|
|
|
|
2018-03-29 20:48:49 +02:00
|
|
|
self.eq = function (idx) {
|
|
|
|
return tabs[idx];
|
2018-03-29 15:05:37 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
return self;
|
2020-07-16 22:35:58 +02:00
|
|
|
})();
|
2018-03-29 15:05:37 +02:00
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const switcher = (function () {
|
|
|
|
const self = {};
|
2018-03-29 15:05:37 +02:00
|
|
|
|
|
|
|
self.stub = true;
|
|
|
|
|
|
|
|
self.children = [];
|
|
|
|
|
2020-04-20 22:41:03 +02:00
|
|
|
self.classList = new Set();
|
|
|
|
|
2018-03-29 15:05:37 +02:00
|
|
|
self.append = function (child) {
|
|
|
|
self.children.push(child);
|
|
|
|
};
|
|
|
|
|
2020-04-20 22:41:03 +02:00
|
|
|
self.addClass = function (c) {
|
|
|
|
self.classList.add(c);
|
|
|
|
self.addedClass = c;
|
|
|
|
};
|
|
|
|
|
2018-03-29 15:05:37 +02:00
|
|
|
self.find = function (sel) {
|
|
|
|
switch (sel) {
|
2020-07-15 00:34:28 +02:00
|
|
|
case ".ind-tab":
|
|
|
|
return ind_tab;
|
|
|
|
default:
|
|
|
|
throw Error("unknown selector: " + sel);
|
2018-03-29 15:05:37 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
return self;
|
2020-07-16 22:35:58 +02:00
|
|
|
})();
|
2018-03-29 15:05:37 +02:00
|
|
|
|
2020-07-15 01:29:15 +02:00
|
|
|
set_global("$", (sel) => {
|
2018-03-29 15:05:37 +02:00
|
|
|
if (sel.stub) {
|
|
|
|
// The component often redundantly re-wraps objects.
|
|
|
|
return sel;
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (sel) {
|
2020-07-15 00:34:28 +02:00
|
|
|
case "<div class='tab-switcher'></div>":
|
|
|
|
return switcher;
|
|
|
|
case "<div class='tab-switcher stream_sorter_toggle'></div>":
|
|
|
|
return switcher;
|
|
|
|
case "<div class='ind-tab' data-tab-key='keyboard-shortcuts' data-tab-id='0' tabindex='0'>translated: Keyboard shortcuts</div>":
|
|
|
|
return make_tab(0);
|
|
|
|
case "<div class='ind-tab' data-tab-key='message-formatting' data-tab-id='1' tabindex='0'>translated: Message formatting</div>":
|
|
|
|
return make_tab(1);
|
|
|
|
case "<div class='ind-tab' data-tab-key='search-operators' data-tab-id='2' tabindex='0'>translated: Search operators</div>":
|
|
|
|
return make_tab(2);
|
|
|
|
default:
|
|
|
|
throw Error("unknown selector: " + sel);
|
2018-03-29 15:05:37 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
let callback_value;
|
2018-04-04 21:06:58 +02:00
|
|
|
|
2019-10-26 01:49:36 +02:00
|
|
|
let widget = null;
|
2018-04-04 21:06:58 +02:00
|
|
|
widget = components.toggle({
|
2018-03-29 15:05:37 +02:00
|
|
|
selected: 0,
|
|
|
|
values: [
|
2020-07-16 22:40:18 +02:00
|
|
|
{label: i18n.t("Keyboard shortcuts"), key: "keyboard-shortcuts"},
|
|
|
|
{label: i18n.t("Message formatting"), key: "message-formatting"},
|
|
|
|
{label: i18n.t("Search operators"), key: "search-operators"},
|
2018-03-29 15:05:37 +02:00
|
|
|
],
|
2020-04-20 22:41:03 +02:00
|
|
|
html_class: "stream_sorter_toggle",
|
2020-07-20 22:18:43 +02:00
|
|
|
callback(name, key) {
|
2018-03-29 15:05:37 +02:00
|
|
|
assert.equal(callback_args, undefined);
|
|
|
|
callback_args = [name, key];
|
2018-04-04 21:06:58 +02:00
|
|
|
|
|
|
|
// The subs code tries to get a widget value in the middle of a
|
|
|
|
// callback, which can lead to obscure bugs.
|
|
|
|
if (widget) {
|
|
|
|
callback_value = widget.value();
|
|
|
|
}
|
2018-03-29 15:05:37 +02:00
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
assert.equal(widget.get(), switcher);
|
|
|
|
|
|
|
|
assert.deepEqual(switcher.children, tabs);
|
|
|
|
|
2020-07-15 01:29:15 +02:00
|
|
|
assert.equal(switcher.addedClass, "stream_sorter_toggle");
|
2020-04-20 22:41:03 +02:00
|
|
|
|
2018-03-29 15:05:37 +02:00
|
|
|
assert.equal(focused_tab, 0);
|
2020-07-15 01:29:15 +02:00
|
|
|
assert.equal(tabs[0].class, "first selected");
|
|
|
|
assert.equal(tabs[1].class, "middle");
|
|
|
|
assert.equal(tabs[2].class, "last");
|
|
|
|
assert.deepEqual(callback_args, ["translated: Keyboard shortcuts", "keyboard-shortcuts"]);
|
|
|
|
assert.equal(widget.value(), "translated: Keyboard shortcuts");
|
2018-03-29 15:05:37 +02:00
|
|
|
|
|
|
|
callback_args = undefined;
|
|
|
|
|
2020-07-15 01:29:15 +02:00
|
|
|
widget.goto("message-formatting");
|
2018-03-29 15:05:37 +02:00
|
|
|
assert.equal(focused_tab, 1);
|
2020-07-15 01:29:15 +02:00
|
|
|
assert.equal(tabs[0].class, "first");
|
|
|
|
assert.equal(tabs[1].class, "middle selected");
|
|
|
|
assert.equal(tabs[2].class, "last");
|
|
|
|
assert.deepEqual(callback_args, ["translated: Message formatting", "message-formatting"]);
|
|
|
|
assert.equal(widget.value(), "translated: Message formatting");
|
2018-03-29 15:05:37 +02:00
|
|
|
|
toggler: Always call back to callback function.
In our toggler component (the thing that handles tabs in things
like our markdown/search help, settings/org, etc.), we have
a callback mechanism when you switch to the tab. We were
being tricky and only calling it when the tab changed.
It turns out it's better to just always call the callback,
since these things are often in modals that open and close,
and if you open a modal for the second time, you want to do
the callback task for whichever setting you're going to.
There was actually kind of a nasty bug with this, where the
keyboard handling in the keyboard-help modal worked fine the
first time you opened it, but then it didn't work the second
time (if you focused some other element in the interim), and
it was due to not re-setting the focus to the inner modal
because we weren't calling the callback.
Of course, there are pitfalls in calling the same callbacks
twice, but our callbacks should generally be idempotent
for other reasons.
2018-06-03 22:12:10 +02:00
|
|
|
// Go to same tab twice and make sure we get callback.
|
2018-03-29 15:05:37 +02:00
|
|
|
callback_args = undefined;
|
2020-07-15 01:29:15 +02:00
|
|
|
widget.goto("message-formatting");
|
|
|
|
assert.deepEqual(callback_args, ["translated: Message formatting", "message-formatting"]);
|
2018-03-29 15:05:37 +02:00
|
|
|
|
toggler: Always call back to callback function.
In our toggler component (the thing that handles tabs in things
like our markdown/search help, settings/org, etc.), we have
a callback mechanism when you switch to the tab. We were
being tricky and only calling it when the tab changed.
It turns out it's better to just always call the callback,
since these things are often in modals that open and close,
and if you open a modal for the second time, you want to do
the callback task for whichever setting you're going to.
There was actually kind of a nasty bug with this, where the
keyboard handling in the keyboard-help modal worked fine the
first time you opened it, but then it didn't work the second
time (if you focused some other element in the interim), and
it was due to not re-setting the focus to the inner modal
because we weren't calling the callback.
Of course, there are pitfalls in calling the same callbacks
twice, but our callbacks should generally be idempotent
for other reasons.
2018-06-03 22:12:10 +02:00
|
|
|
callback_args = undefined;
|
2018-03-29 15:05:37 +02:00
|
|
|
keydown_f.call(tabs[focused_tab], RIGHT_KEY);
|
|
|
|
assert.equal(focused_tab, 2);
|
2020-07-15 01:29:15 +02:00
|
|
|
assert.equal(tabs[0].class, "first");
|
|
|
|
assert.equal(tabs[1].class, "middle");
|
|
|
|
assert.equal(tabs[2].class, "last selected");
|
|
|
|
assert.deepEqual(callback_args, ["translated: Search operators", "search-operators"]);
|
|
|
|
assert.equal(widget.value(), "translated: Search operators");
|
2018-04-04 21:06:58 +02:00
|
|
|
assert.equal(widget.value(), callback_value);
|
2018-03-29 15:05:37 +02:00
|
|
|
|
|
|
|
// try to crash the key handler
|
|
|
|
keydown_f.call(tabs[focused_tab], RIGHT_KEY);
|
2020-07-15 01:29:15 +02:00
|
|
|
assert.equal(widget.value(), "translated: Search operators");
|
2018-03-29 15:05:37 +02:00
|
|
|
|
|
|
|
callback_args = undefined;
|
|
|
|
|
|
|
|
keydown_f.call(tabs[focused_tab], LEFT_KEY);
|
2020-07-15 01:29:15 +02:00
|
|
|
assert.equal(widget.value(), "translated: Message formatting");
|
2018-03-29 15:05:37 +02:00
|
|
|
|
|
|
|
callback_args = undefined;
|
|
|
|
|
|
|
|
keydown_f.call(tabs[focused_tab], LEFT_KEY);
|
2020-07-15 01:29:15 +02:00
|
|
|
assert.equal(widget.value(), "translated: Keyboard shortcuts");
|
2018-03-29 15:05:37 +02:00
|
|
|
|
|
|
|
// try to crash the key handler
|
|
|
|
keydown_f.call(tabs[focused_tab], LEFT_KEY);
|
2020-07-15 01:29:15 +02:00
|
|
|
assert.equal(widget.value(), "translated: Keyboard shortcuts");
|
2018-03-29 20:48:49 +02:00
|
|
|
|
|
|
|
callback_args = undefined;
|
|
|
|
|
|
|
|
click_f.call(tabs[1]);
|
2020-07-15 01:29:15 +02:00
|
|
|
assert.equal(widget.value(), "translated: Message formatting");
|
2019-05-08 09:26:27 +02:00
|
|
|
|
|
|
|
callback_args = undefined;
|
|
|
|
widget.disable_tab("search-operators");
|
2020-07-15 01:29:15 +02:00
|
|
|
assert.equal(tabs[2].hasClass("disabled"), true);
|
2019-05-08 09:26:27 +02:00
|
|
|
assert.equal(tabs[2].class, "last disabled");
|
|
|
|
|
2020-07-15 01:29:15 +02:00
|
|
|
widget.goto("keyboard-shortcuts");
|
2019-05-08 09:26:27 +02:00
|
|
|
assert.equal(focused_tab, 0);
|
|
|
|
widget.goto("search-operators");
|
|
|
|
assert.equal(focused_tab, 0);
|
2018-05-15 12:40:07 +02:00
|
|
|
});
|