修改方法查询效果实现 #63

Merged
obugs merged 1 commits from check into main 2026-10-05 18:56:43 +08:00
5 changed files with 1833 additions and 1041 deletions
+4 -2
View File
@@ -97,13 +97,15 @@
<input class="cm-query" type="text" placeholder="输入自定义方法名,如 ChangeCharacter" autocomplete="off"> <input class="cm-query" type="text" placeholder="输入自定义方法名,如 ChangeCharacter" autocomplete="off">
<ul class="cm-suggest"></ul> <ul class="cm-suggest"></ul>
</div> </div>
<div class="cm-options"></div>
<div class="cm-selected" hidden> <div class="cm-selected" hidden>
<div class="cm-head"> <div class="cm-head">
<span class="cm-name"></span> <span class="cm-name"></span>
<span class="cm-desc"></span> <span class="cm-short"></span>
</div> </div>
<div class="cm-row"></div> <div class="cm-call"></div>
<code class="cm-preview"></code> <code class="cm-preview"></code>
<div class="cm-desc"></div>
</div> </div>
</div> </div>
+1149 -622
View File
File diff suppressed because it is too large Load Diff
+275 -190
View File
@@ -50,55 +50,56 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
} }
} }
class Method { class Method {
constructor(name, type, description) { constructor(name, type, shortDescription, description) {
this.name = name; this.name = name;
this.type = type; this.type = type;
this.shortDescription = shortDescription;
this.description = description; this.description = description;
} }
} }
class Mod extends Method { class Mod extends Method {
constructor(name, description) { constructor(name, shortDescription, description) {
super(name, MethodType.Mod, description); super(name, MethodType.Mod, shortDescription, description);
} }
} }
class Surgery extends Method { class Surgery extends Method {
constructor(name, description) { constructor(name, shortDescription, description) {
super(name, MethodType.Surgery, description); super(name, MethodType.Surgery, shortDescription, description);
} }
} }
class CommentMethod extends Method { class CommentMethod extends Method {
constructor(name, description, parameters) { constructor(name, shortDescription, description, parameters) {
super(name, MethodType.Comment, description); super(name, MethodType.Comment, shortDescription, description);
this.parameters = parameters; this.parameters = parameters;
} }
} }
class InkMethod extends Method { class InkMethod extends Method {
constructor(name, description, parameters) { constructor(name, shortDescription, description, parameters) {
super(name, MethodType.Ink, description); super(name, MethodType.Ink, shortDescription, description);
this.parameters = parameters; this.parameters = parameters;
} }
} }
class CustomMethods extends Method { class CustomMethods extends Method {
constructor(name, description, parameters) { constructor(name, shortDescription, description, parameters) {
super(name, MethodType.CustomMethod, description); super(name, MethodType.CustomMethod, shortDescription, description);
this.parameters = parameters; this.parameters = parameters;
} }
} }
class VariableMethod extends Method { class VariableMethod extends Method {
constructor(name, description, parameters) { constructor(name, shortDescription, description, parameters) {
super(name, MethodType.Variable, description); super(name, MethodType.Variable, shortDescription, description);
this.parameters = parameters; this.parameters = parameters;
} }
} }
class ConditionMethod extends Method { class ConditionMethod extends Method {
constructor(name, description, parameters) { constructor(name, shortDescription, description, parameters) {
super(name, MethodType.Condition, description); super(name, MethodType.Condition, shortDescription, description);
this.parameters = parameters; this.parameters = parameters;
} }
} }
class CustomClassMethod extends Method { class CustomClassMethod extends Method {
constructor(name, description) { constructor(name, shortDescription, description) {
super(name, MethodType.CustomClass, description); super(name, MethodType.CustomClass, shortDescription, description);
} }
} }
function editDistance(a, b, caseSensitive = true) { function editDistance(a, b, caseSensitive = true) {
@@ -131,41 +132,19 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
var _a; var _a;
return (_a = method.parameters) !== null && _a !== void 0 ? _a : []; return (_a = method.parameters) !== null && _a !== void 0 ? _a : [];
} }
// 全局策略:字符串写法、数字/布尔默认的常量/表达式状态
const strategies = {
string: "prefix",
number: "constant",
boolean: "constant",
};
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
// 数据:可直接在此编辑,或通过 window.CustomMethodSearch.setMethods(...) 覆盖 // 外置数据 hydrate(script/custommethod-data.js)
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
let methods = [
new Mod("wobblyLines", "让单发拍子的轨道抖动"),
new Mod("invisibleHeart", "隐藏轨道上的心脏"),
new CustomMethods("ChangeCharacter", "修改某条轨道对应的人物", [
new Parameter("character", ParameterType.Enum, "Characters"),
new Parameter("row", ParameterType.Number, "轨道编号", undefined, new NumericRange(0, 15, true)),
]),
new CustomMethods("DisableRowReflections", "开关某个房间的地面反射", [
new Parameter("room", ParameterType.Number, "房间编号", undefined, new NumericRange(0, 3, true)),
]),
new CustomMethods("SetRowLength", "设置轨道长度", [
new Parameter("row", ParameterType.Number, "轨道编号", undefined, new NumericRange(0, 15, true)),
new Parameter("length", ParameterType.Number, "轨道长度", undefined, new NumericRange(1, 7, true)),
]),
new CommentMethod("trueCameraMove", "移动真实摄像头", [
new Parameter("x", ParameterType.Number, "目标 X"),
new Parameter("y", ParameterType.Number, "目标 Y"),
new Parameter("angle", ParameterType.Number, "角度"),
new Parameter("duration", ParameterType.Number, "时长"),
new Parameter("ease", ParameterType.Enum, "缓动类型", "EaseType"),
]),
new CustomMethods("DemoAllTypes", "演示四种参数控件", [
new Parameter("text", ParameterType.String, "文本"),
new Parameter("count", ParameterType.Number, "数字"),
new Parameter("enabled", ParameterType.Boolean, "开关"),
new Parameter("character", ParameterType.Enum, "角色", "Characters"),
new Parameter("color", ParameterType.Enum, "颜色"),
]),
];
// 合并外置数据(script/custommethod-data.js),同名以内置为准
function hydrateMethod(raw) { function hydrateMethod(raw) {
var _a, _b, _c, _d, _e, _f, _g, _h; var _a, _b, _c, _d;
const shortDescription = (_b = (_a = raw.shortDescription) !== null && _a !== void 0 ? _a : raw.description) !== null && _b !== void 0 ? _b : "";
const description = (_d = (_c = raw.description) !== null && _c !== void 0 ? _c : raw.shortDescription) !== null && _d !== void 0 ? _d : "";
const parameters = Array.isArray(raw.parameters) const parameters = Array.isArray(raw.parameters)
? raw.parameters.map((item) => { ? raw.parameters.map((item) => {
var _a, _b; var _a, _b;
@@ -175,16 +154,17 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
}) })
: []; : [];
switch (raw.type) { switch (raw.type) {
case MethodType.Mod: return new Mod(raw.name, (_a = raw.description) !== null && _a !== void 0 ? _a : ""); case MethodType.Mod: return new Mod(raw.name, shortDescription, description);
case MethodType.Surgery: return new Surgery(raw.name, (_b = raw.description) !== null && _b !== void 0 ? _b : ""); case MethodType.Surgery: return new Surgery(raw.name, shortDescription, description);
case MethodType.Comment: return new CommentMethod(raw.name, (_c = raw.description) !== null && _c !== void 0 ? _c : "", parameters); case MethodType.Comment: return new CommentMethod(raw.name, shortDescription, description, parameters);
case MethodType.Ink: return new InkMethod(raw.name, (_d = raw.description) !== null && _d !== void 0 ? _d : "", parameters); case MethodType.Ink: return new InkMethod(raw.name, shortDescription, description, parameters);
case MethodType.Variable: return new VariableMethod(raw.name, (_e = raw.description) !== null && _e !== void 0 ? _e : "", parameters); case MethodType.Variable: return new VariableMethod(raw.name, shortDescription, description, parameters);
case MethodType.Condition: return new ConditionMethod(raw.name, (_f = raw.description) !== null && _f !== void 0 ? _f : "", parameters); case MethodType.Condition: return new ConditionMethod(raw.name, shortDescription, description, parameters);
case MethodType.CustomClass: return new CustomClassMethod(raw.name, (_g = raw.description) !== null && _g !== void 0 ? _g : ""); case MethodType.CustomClass: return new CustomClassMethod(raw.name, shortDescription, description);
default: return new CustomMethods(raw.name, (_h = raw.description) !== null && _h !== void 0 ? _h : "", parameters); default: return new CustomMethods(raw.name, shortDescription, description, parameters);
} }
} }
let methods = [];
function loadExternalData() { function loadExternalData() {
const data = window.CustomMethodSearchData; const data = window.CustomMethodSearchData;
if (!data) if (!data)
@@ -247,7 +227,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
.map((item) => item.method); .map((item) => item.method);
} }
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
// DOM 构建 // DOM 工具
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
function el(tag, className) { function el(tag, className) {
const node = document.createElement(tag); const node = document.createElement(tag);
@@ -256,27 +236,19 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
return node; return node;
} }
function resolveEnumOptions(parameter) { function resolveEnumOptions(parameter) {
if (parameter.enumType && enumTypeMap[parameter.enumType]) { if (parameter.enumType && enumTypeMap[parameter.enumType])
return enumTypeMap[parameter.enumType]; return enumTypeMap[parameter.enumType];
} if (enumTypeMap[parameter.description])
// 兼容把枚举类型名写在 description 的情况
if (enumTypeMap[parameter.description]) {
return enumTypeMap[parameter.description]; return enumTypeMap[parameter.description];
} if (enumTypeMap[parameter.name])
if (enumTypeMap[parameter.name]) {
return enumTypeMap[parameter.name]; return enumTypeMap[parameter.name];
}
return []; return [];
} }
// 布尔值可用 true/false,也可用布尔变量 b0~b9 // 布尔可用 true/false,或布尔变量 b0~b9
const BOOLEAN_OPTIONS = ["true", "false"]; const BOOLEAN_OPTIONS = ["true", "false"];
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
BOOLEAN_OPTIONS.push("b" + i); BOOLEAN_OPTIONS.push("b" + i);
} }
function isNumberVariable(value) {
// 数字位置允许的变量:i0~i9(整型)、f0~f9(浮点)、b0~b9(布尔)
return /^[ifb][0-9]$/.test(value);
}
function makeCombo(parameter, options, onChange) { function makeCombo(parameter, options, onChange) {
const combo = el("span", "cm-combo"); const combo = el("span", "cm-combo");
const input = el("input", "cm-input cm-combo-input"); const input = el("input", "cm-input cm-combo-input");
@@ -316,112 +288,177 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
} }
list.hidden = false; list.hidden = false;
} }
input.addEventListener("input", () => { input.addEventListener("input", () => { populate(); onChange(); });
populate();
onChange();
});
input.addEventListener("focus", populate); input.addEventListener("focus", populate);
input.addEventListener("blur", () => { input.addEventListener("blur", () => { window.setTimeout(() => { list.hidden = true; }, 120); });
window.setTimeout(() => { list.hidden = true; }, 120);
});
combo.appendChild(input); combo.appendChild(input);
combo.appendChild(list); combo.appendChild(list);
return combo; return combo;
} }
function makeControl(parameter, onChange) { function makeModeToggle(initial, onChange) {
const button = el("button", "cm-mode");
button.type = "button";
let mode = initial;
const render = () => {
button.textContent = mode === "expression" ? "表达式" : "常量";
button.title = "切换 常量 / 表达式";
button.classList.toggle("is-expression", mode === "expression");
};
render();
button.addEventListener("click", (event) => {
event.preventDefault();
mode = mode === "expression" ? "constant" : "expression";
render();
onChange();
});
return { el: button, get: () => mode };
}
function stringValue(value) {
if (value.length === 0)
return "<文本>";
return strategies.string === "quote" ? `"${value}"` : `str:${value}`;
}
function makeField(parameter, onChange) {
switch (parameter.type) { switch (parameter.type) {
case ParameterType.Boolean: { case ParameterType.Boolean: {
return makeCombo(parameter, BOOLEAN_OPTIONS, onChange); const wrap = el("span", "cm-field");
const checkbox = el("input", "cm-input cm-check");
checkbox.type = "checkbox";
checkbox.addEventListener("change", onChange);
const combo = makeCombo(parameter, BOOLEAN_OPTIONS, onChange);
const comboInput = combo.querySelector("input");
let mode = strategies.boolean;
const toggle = makeModeToggle(strategies.boolean, () => {
mode = toggle.get();
applyMode();
onChange();
});
function applyMode() {
const expression = mode === "expression";
checkbox.hidden = expression;
combo.hidden = !expression;
} }
case ParameterType.Enum: { applyMode();
// 可输入的下拉框:输入时用编辑距离筛选候选项 wrap.appendChild(toggle.el);
return makeCombo(parameter, resolveEnumOptions(parameter), onChange); wrap.appendChild(checkbox);
wrap.appendChild(combo);
return {
el: wrap,
read: () => {
if (mode === "constant") {
return { text: checkbox.checked ? "true" : "false", valid: true, invalid: false };
}
const value = comboInput.value.trim();
return value.length > 0
? { text: value, valid: true, invalid: false }
: { text: "<布尔>", valid: false, invalid: true };
},
};
} }
case ParameterType.Number: { case ParameterType.Number: {
// 普通输入框,合法性由 readParameter 校验(非数字标红) const wrap = el("span", "cm-field");
const number = el("input", "cm-input"); const input = el("input", "cm-input");
number.type = "text"; input.type = "text";
number.inputMode = "decimal"; input.inputMode = "decimal";
number.placeholder = parameter.name; input.placeholder = parameter.name;
number.addEventListener("input", onChange); input.addEventListener("input", onChange);
return number; const mode = makeModeToggle(strategies.number, onChange);
} wrap.appendChild(mode.el);
default: { wrap.appendChild(input);
const text = el("input", "cm-input"); return {
text.type = "text"; el: wrap,
text.placeholder = parameter.name; read: () => {
text.addEventListener("input", onChange);
return text;
}
}
}
function readParameter(parameter, control) {
switch (parameter.type) {
case ParameterType.Boolean: {
const input = control.querySelector("input");
const value = input.value.trim(); const value = input.value.trim();
const valid = value === "true" || value === "false" || /^b[0-9]$/.test(value); if (value.length === 0)
return { text: value || "<布尔>", valid, invalid: value.length > 0 && !valid, control }; return { text: "<数字>", valid: false, invalid: true };
} if (mode.get() === "expression")
case ParameterType.Enum: { return { text: value, valid: true, invalid: false };
const input = control.querySelector("input");
const value = input.value.trim();
const options = resolveEnumOptions(parameter);
const valid = options.length > 0
? options.some((option) => option.toLowerCase() === value.toLowerCase())
: value.length > 0;
return { text: value || "<选项>", valid, invalid: value.length > 0 && !valid, control };
}
case ParameterType.Number: {
const input = control;
const value = input.value.trim();
if (value.length === 0) {
return { text: "<数字>", valid: false, invalid: false, control };
}
if (isNumberVariable(value)) {
return { text: value, valid: true, invalid: false, control };
}
const number = Number(value); const number = Number(value);
let valid = isFinite(number); let valid = isFinite(number);
const range = parameter.numericRange; const range = parameter.numericRange;
if (valid && range) { if (valid && range) {
if (range.isInteger && !Number.isInteger(number)) if (range.isInteger && !Number.isInteger(number))
valid = false; valid = false;
if (number < range.min || number > range.max) if (range.min !== undefined && number < range.min)
valid = false;
if (range.max !== undefined && number > range.max)
valid = false; valid = false;
} }
return { text: value, valid, invalid: !valid, control }; return { text: value, valid, invalid: !valid };
},
};
}
case ParameterType.Enum: {
const wrap = el("span", "cm-field cm-field-string");
const open = el("span", "cm-lit");
const close = el("span", "cm-lit");
open.textContent = strategies.string === "quote" ? "\"" : "str:";
close.textContent = strategies.string === "quote" ? "\"" : "";
const combo = makeCombo(parameter, resolveEnumOptions(parameter), onChange);
const input = combo.querySelector("input");
wrap.appendChild(open);
wrap.appendChild(combo);
wrap.appendChild(close);
return {
el: wrap,
read: () => {
const value = input.value.trim();
const options = resolveEnumOptions(parameter);
const valid = options.length > 0
? options.some((option) => option.toLowerCase() === value.toLowerCase())
: value.length > 0;
return { text: stringValue(value), valid, invalid: !valid };
},
};
} }
default: { default: {
const input = control; const wrap = el("span", "cm-field cm-field-string");
const open = el("span", "cm-lit");
const close = el("span", "cm-lit");
open.textContent = strategies.string === "quote" ? "\"" : "str:";
close.textContent = strategies.string === "quote" ? "\"" : "";
const input = el("input", "cm-input");
input.type = "text";
input.placeholder = parameter.name;
input.addEventListener("input", onChange);
wrap.appendChild(open);
wrap.appendChild(input);
wrap.appendChild(close);
return {
el: wrap,
read: () => {
const value = input.value.trim(); const value = input.value.trim();
return { text: value ? `str:${value}` : "<文本>", valid: value.length > 0, invalid: false, control }; return { text: stringValue(value), valid: value.length > 0, invalid: value.length === 0 };
},
};
} }
} }
} }
function stripStringPrefix(text) { function methodSkeleton(method, hasParams) {
return text.startsWith("str:") ? text.slice(4) : text; switch (method.type) {
case MethodType.Comment: return { prefix: `()=>${method.name}(`, suffix: ")" };
case MethodType.Ink: return { prefix: `DOTHIS{${method.name}(`, suffix: ")}" };
case MethodType.Variable: return { prefix: `${method.name} = `, suffix: "" };
case MethodType.Mod: return { prefix: `"mods": [..., "`, suffix: `"]` };
case MethodType.CustomClass: return { prefix: `"customClass": "`, suffix: `"` };
case MethodType.Condition:
return hasParams ? { prefix: `${method.name}(`, suffix: ")" } : { prefix: method.name, suffix: "" };
case MethodType.Surgery: return { prefix: method.name, suffix: "" };
default:
return hasParams ? { prefix: `${method.name}(`, suffix: ")" } : { prefix: method.name, suffix: "" };
}
} }
function formatMethod(method, states) { function formatMethod(method, states) {
const values = states.map((state) => state.text); const args = states.map((state) => state.text).join(", ");
const args = values.join(", ");
switch (method.type) { switch (method.type) {
case MethodType.Comment: case MethodType.Comment: return `()=>${method.name}(${args})`;
return `()=>${method.name}(${args})`; case MethodType.Ink: return `DOTHIS{${method.name}(${args})}`;
case MethodType.Ink: case MethodType.Variable: return `${method.name} = ${states.length ? states[0].text : ""}`;
return `DOTHIS{${method.name}(${args})}`; case MethodType.Mod: return `"mods": [..., "${method.name}"]`;
case MethodType.Variable: case MethodType.CustomClass: return `"customClass": "${method.name}"`;
return `${method.name} = ${values.length ? stripStringPrefix(values[0]) : ""}`; case MethodType.Condition: return states.length ? `${method.name}(${args})` : method.name;
case MethodType.Mod: case MethodType.Surgery: return method.name;
return `"mods": [..., "${method.name}"]`; default: return states.length ? `${method.name}(${args})` : method.name;
case MethodType.CustomClass:
return `"customClass": "${method.name}"`;
case MethodType.Condition:
return values.length ? `${method.name}(${args})` : method.name;
case MethodType.Surgery:
return method.name;
default:
return values.length ? `${method.name}(${args})` : method.name;
} }
} }
function copyText(text) { function copyText(text) {
@@ -433,7 +470,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
} }
} }
catch (error) { catch (error) {
// fallthrough to execCommand // fall through
} }
try { try {
const area = document.createElement("textarea"); const area = document.createElement("textarea");
@@ -451,19 +488,24 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
} }
}); });
} }
// ---------------------------------------------------------------------
// 渲染
// ---------------------------------------------------------------------
function renderContainer(container) { function renderContainer(container) {
if (container.dataset.cmReady === "1") if (container.dataset.cmReady === "1")
return; return;
container.dataset.cmReady = "1"; container.dataset.cmReady = "1";
// 优先使用 custommethod.md 里写好的骨架;缺失时再动态生成 let currentMethod = null;
let query = container.querySelector(".cm-query"); let query = container.querySelector(".cm-query");
let suggest = container.querySelector(".cm-suggest"); let suggest = container.querySelector(".cm-suggest");
let selected = container.querySelector(".cm-selected"); let selected = container.querySelector(".cm-selected");
let nameEl = container.querySelector(".cm-name"); let nameEl = container.querySelector(".cm-name");
let descEl = container.querySelector(".cm-desc"); let shortEl = container.querySelector(".cm-short");
let row = container.querySelector(".cm-row"); let call = container.querySelector(".cm-call");
let preview = container.querySelector(".cm-preview"); let preview = container.querySelector(".cm-preview");
if (!query || !suggest || !selected || !nameEl || !descEl || !row || !preview) { let desc = container.querySelector(".cm-desc");
let options = container.querySelector(".cm-options");
if (!query || !suggest || !selected || !nameEl || !shortEl || !call || !preview || !desc) {
container.textContent = ""; container.textContent = "";
const searchBar = el("div", "cm-search"); const searchBar = el("div", "cm-search");
query = el("input", "cm-query"); query = el("input", "cm-query");
@@ -477,32 +519,68 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
selected.hidden = true; selected.hidden = true;
const head = el("div", "cm-head"); const head = el("div", "cm-head");
nameEl = el("span", "cm-name"); nameEl = el("span", "cm-name");
descEl = el("span", "cm-desc"); shortEl = el("span", "cm-short");
head.appendChild(nameEl); head.appendChild(nameEl);
head.appendChild(descEl); head.appendChild(shortEl);
row = el("div", "cm-row"); call = el("div", "cm-call");
preview = el("code", "cm-preview"); preview = el("code", "cm-preview");
desc = el("div", "cm-desc");
selected.appendChild(head); selected.appendChild(head);
selected.appendChild(row); selected.appendChild(call);
selected.appendChild(preview); selected.appendChild(preview);
selected.appendChild(desc);
container.appendChild(searchBar); container.appendChild(searchBar);
container.appendChild(selected); container.appendChild(selected);
} }
// 搜索框下的策略选项
options = container.querySelector(".cm-options");
if (!options) {
options = el("div", "cm-options");
query.parentElement.insertAdjacentElement("afterend", options);
}
if (!options.querySelector(".cm-option-select")) {
const optionsBox = options;
function addOption(label, key, values, current) {
const wrap = el("label", "cm-option");
const text = el("span", "cm-option-label");
text.textContent = label;
const select = el("select", "cm-option-select");
for (const [value, caption] of values) {
const opt = el("option");
opt.value = value;
opt.textContent = caption;
select.appendChild(opt);
}
select.value = current;
select.addEventListener("change", () => {
strategies[key] = select.value;
if (currentMethod) {
selectMethod(currentMethod);
}
});
wrap.appendChild(text);
wrap.appendChild(select);
optionsBox.appendChild(wrap);
}
addOption("字符串", "string", [["prefix", "str: 前缀"], ["quote", "引号 \"…\""]], strategies.string);
}
const queryEl = query; const queryEl = query;
const suggestEl = suggest; const suggestEl = suggest;
const selectedEl = selected; const selectedEl = selected;
const nameNode = nameEl; const nameNode = nameEl;
const descNode = descEl; const shortNode = shortEl;
const rowEl = row; const callEl = call;
const previewEl = preview; const previewEl = preview;
const descNode = desc;
const copyBtn = el("button", "cm-copy"); const copyBtn = el("button", "cm-copy");
copyBtn.type = "button"; copyBtn.type = "button";
copyBtn.textContent = "复制"; copyBtn.textContent = "复制";
copyBtn.hidden = true; copyBtn.hidden = true;
copyBtn.title = "复制生成的调用代码"; copyBtn.title = "复制生成的代码";
selectedEl.appendChild(copyBtn); selectedEl.appendChild(copyBtn);
copyBtn.addEventListener("click", () => { copyBtn.addEventListener("click", () => {
copyText(previewEl.textContent).then((ok) => { var _a;
copyText((_a = previewEl.textContent) !== null && _a !== void 0 ? _a : "").then((ok) => {
copyBtn.textContent = ok ? "已复制" : "复制失败"; copyBtn.textContent = ok ? "已复制" : "复制失败";
window.setTimeout(() => { copyBtn.textContent = "复制"; }, 1200); window.setTimeout(() => { copyBtn.textContent = "复制"; }, 1200);
}); });
@@ -511,51 +589,61 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
suggestEl.textContent = ""; suggestEl.textContent = "";
} }
function selectMethod(method) { function selectMethod(method) {
currentMethod = method;
clearSuggest(); clearSuggest();
queryEl.value = method.name; queryEl.value = method.name;
selectedEl.hidden = false; selectedEl.hidden = false;
nameNode.textContent = method.name; nameNode.textContent = method.name;
descNode.textContent = method.description; shortNode.textContent = method.shortDescription || "";
rowEl.textContent = ""; descNode.textContent = method.description || "";
callEl.textContent = "";
previewEl.textContent = ""; previewEl.textContent = "";
copyBtn.hidden = true; copyBtn.hidden = true;
const parameters = parametersOf(method); const parameters = parametersOf(method);
const controls = []; const fields = [];
function updatePreview() { function updatePreview() {
const states = controls.map((item) => readParameter(item.parameter, item.control)); const states = fields.map((field) => field.read());
states.forEach((state) => state.control.classList.toggle("cm-invalid", state.invalid)); fields.forEach((field, i) => field.el.classList.toggle("cm-invalid", states[i].invalid));
previewEl.textContent = formatMethod(method, states); previewEl.textContent = formatMethod(method, states);
copyBtn.hidden = !states.every((state) => state.valid); copyBtn.hidden = !states.every((state) => state.valid);
} }
const skeleton = methodSkeleton(method, parameters.length > 0);
const code = el("code", "cm-code");
const prefix = el("span", "cm-lit");
prefix.textContent = skeleton.prefix;
code.appendChild(prefix);
parameters.forEach((parameter, index) => {
if (index > 0) {
const sep = el("span", "cm-lit");
sep.textContent = ", ";
code.appendChild(sep);
}
const field = makeField(parameter, updatePreview);
field.el.title = parameter.description;
code.appendChild(field.el);
fields.push(field);
});
if (skeleton.suffix) {
const suffix = el("span", "cm-lit");
suffix.textContent = skeleton.suffix;
code.appendChild(suffix);
}
callEl.appendChild(code);
if (parameters.length === 0) { if (parameters.length === 0) {
const empty = el("span", "cm-empty"); const empty = el("span", "cm-empty");
empty.textContent = "该方法没有参数"; empty.textContent = "(无参数)";
rowEl.appendChild(empty); callEl.appendChild(empty);
updatePreview();
return;
}
for (const parameter of parameters) {
const field = el("label", "cm-param");
field.title = parameter.description;
const label = el("span", "cm-param-name");
label.textContent = parameter.name;
const control = makeControl(parameter, updatePreview);
field.appendChild(label);
field.appendChild(control);
rowEl.appendChild(field);
controls.push({ parameter, control });
} }
updatePreview(); updatePreview();
} }
function updateSuggestions() { function updateSuggestions() {
clearSuggest(); clearSuggest();
const matches = searchMethods(queryEl.value); for (const method of searchMethods(queryEl.value)) {
for (const method of matches) {
const item = el("li", "cm-suggest-item"); const item = el("li", "cm-suggest-item");
const nameSpan = el("span", "cm-suggest-name"); const nameSpan = el("span", "cm-suggest-name");
nameSpan.textContent = method.name; nameSpan.textContent = method.name;
const descSpan = el("span", "cm-suggest-desc"); const descSpan = el("span", "cm-suggest-desc");
descSpan.textContent = method.description; descSpan.textContent = method.shortDescription || method.description;
item.appendChild(nameSpan); item.appendChild(nameSpan);
item.appendChild(descSpan); item.appendChild(descSpan);
item.addEventListener("mousedown", (event) => { item.addEventListener("mousedown", (event) => {
@@ -578,8 +666,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
} }
} }
function initAll() { function initAll() {
const containers = document.querySelectorAll(".custommethods-search"); document.querySelectorAll(".custommethods-search").forEach((container) => renderContainer(container));
containers.forEach((container) => renderContainer(container));
} }
function register() { function register() {
const docsify = window.$docsify; const docsify = window.$docsify;
@@ -599,11 +686,9 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
search: searchMethods, search: searchMethods,
editDistance, editDistance,
renderAll: initAll, renderAll: initAll,
strategies,
getMethods: () => methods, getMethods: () => methods,
setMethods: (list) => { setMethods: (list) => { methods = list; initAll(); },
methods = list;
initAll();
},
Parameter, Parameter,
ParameterType, ParameterType,
Method, Method,
+293 -207
View File
@@ -17,17 +17,20 @@
Condition = "condition", Condition = "condition",
CustomClass = "custom_class", CustomClass = "custom_class",
} }
type ValueMode = "constant" | "expression";
type StringStrategy = "prefix" | "quote";
const enumTypeMap: Record<string, string[]> = { const enumTypeMap: Record<string, string[]> = {
Characters: ["Logan", "Green", "Blue", "Yellow", "Purple", "Orange"], Characters: ["Logan", "Green", "Blue", "Yellow", "Purple", "Orange"],
Size: ["Small", "Medium", "Large"], Size: ["Small", "Medium", "Large"],
Shape: ["Circle", "Square", "Triangle"], Shape: ["Circle", "Square", "Triangle"],
}; };
class NumericRange { class NumericRange {
min: number; min?: number;
max: number; max?: number;
isInteger: boolean; isInteger: boolean;
public constructor(min: number, max: number, isInteger: boolean = false) { public constructor(min?: number, max?: number, isInteger: boolean = false) {
this.min = min; this.min = min;
this.max = max; this.max = max;
this.isInteger = isInteger; this.isInteger = isInteger;
@@ -50,61 +53,63 @@ class NumericRange {
class Method { class Method {
name: string; name: string;
type: MethodType; type: MethodType;
shortDescription: string;
description: string; description: string;
public constructor(name: string, type: MethodType, description: string) { public constructor(name: string, type: MethodType, shortDescription: string, description: string) {
this.name = name; this.name = name;
this.type = type; this.type = type;
this.shortDescription = shortDescription;
this.description = description; this.description = description;
} }
} }
class Mod extends Method { class Mod extends Method {
public constructor(name: string, description: string) { public constructor(name: string, shortDescription: string, description: string) {
super(name, MethodType.Mod, description); super(name, MethodType.Mod, shortDescription, description);
} }
} }
class Surgery extends Method { class Surgery extends Method {
public constructor(name: string, description: string) { public constructor(name: string, shortDescription: string, description: string) {
super(name, MethodType.Surgery, description); super(name, MethodType.Surgery, shortDescription, description);
} }
} }
class CommentMethod extends Method { class CommentMethod extends Method {
parameters: Parameter[]; parameters: Parameter[];
public constructor(name: string, description: string, parameters: Parameter[]) { public constructor(name: string, shortDescription: string, description: string, parameters: Parameter[]) {
super(name, MethodType.Comment, description); super(name, MethodType.Comment, shortDescription, description);
this.parameters = parameters; this.parameters = parameters;
} }
} }
class InkMethod extends Method { class InkMethod extends Method {
parameters: Parameter[]; parameters: Parameter[];
public constructor(name: string, description: string, parameters: Parameter[]) { public constructor(name: string, shortDescription: string, description: string, parameters: Parameter[]) {
super(name, MethodType.Ink, description); super(name, MethodType.Ink, shortDescription, description);
this.parameters = parameters; this.parameters = parameters;
} }
} }
class CustomMethods extends Method { class CustomMethods extends Method {
parameters: Parameter[]; parameters: Parameter[];
public constructor(name: string, description: string, parameters: Parameter[]) { public constructor(name: string, shortDescription: string, description: string, parameters: Parameter[]) {
super(name, MethodType.CustomMethod, description); super(name, MethodType.CustomMethod, shortDescription, description);
this.parameters = parameters; this.parameters = parameters;
} }
} }
class VariableMethod extends Method { class VariableMethod extends Method {
parameters: Parameter[]; parameters: Parameter[];
public constructor(name: string, description: string, parameters: Parameter[]) { public constructor(name: string, shortDescription: string, description: string, parameters: Parameter[]) {
super(name, MethodType.Variable, description); super(name, MethodType.Variable, shortDescription, description);
this.parameters = parameters; this.parameters = parameters;
} }
} }
class ConditionMethod extends Method { class ConditionMethod extends Method {
parameters: Parameter[]; parameters: Parameter[];
public constructor(name: string, description: string, parameters: Parameter[]) { public constructor(name: string, shortDescription: string, description: string, parameters: Parameter[]) {
super(name, MethodType.Condition, description); super(name, MethodType.Condition, shortDescription, description);
this.parameters = parameters; this.parameters = parameters;
} }
} }
class CustomClassMethod extends Method { class CustomClassMethod extends Method {
public constructor(name: string, description: string) { public constructor(name: string, shortDescription: string, description: string) {
super(name, MethodType.CustomClass, description); super(name, MethodType.CustomClass, shortDescription, description);
} }
} }
@@ -139,41 +144,19 @@ class NumericRange {
return (method as CommentMethod | InkMethod | CustomMethods | VariableMethod | ConditionMethod).parameters ?? []; return (method as CommentMethod | InkMethod | CustomMethods | VariableMethod | ConditionMethod).parameters ?? [];
} }
// --------------------------------------------------------------------- // 全局策略:字符串写法、数字/布尔默认的常量/表达式状态
// 数据:可直接在此编辑,或通过 window.CustomMethodSearch.setMethods(...) 覆盖 const strategies: { string: StringStrategy; number: ValueMode; boolean: ValueMode } = {
// --------------------------------------------------------------------- string: "prefix",
let methods: Method[] = [ number: "constant",
new Mod("wobblyLines", "让单发拍子的轨道抖动"), boolean: "constant",
new Mod("invisibleHeart", "隐藏轨道上的心脏"), };
new CustomMethods("ChangeCharacter", "修改某条轨道对应的人物", [
new Parameter("character", ParameterType.Enum, "Characters"),
new Parameter("row", ParameterType.Number, "轨道编号", undefined, new NumericRange(0, 15, true)),
]),
new CustomMethods("DisableRowReflections", "开关某个房间的地面反射", [
new Parameter("room", ParameterType.Number, "房间编号", undefined, new NumericRange(0, 3, true)),
]),
new CustomMethods("SetRowLength", "设置轨道长度", [
new Parameter("row", ParameterType.Number, "轨道编号", undefined, new NumericRange(0, 15, true)),
new Parameter("length", ParameterType.Number, "轨道长度", undefined, new NumericRange(1, 7, true)),
]),
new CommentMethod("trueCameraMove", "移动真实摄像头", [
new Parameter("x", ParameterType.Number, "目标 X"),
new Parameter("y", ParameterType.Number, "目标 Y"),
new Parameter("angle", ParameterType.Number, "角度"),
new Parameter("duration", ParameterType.Number, "时长"),
new Parameter("ease", ParameterType.Enum, "缓动类型", "EaseType"),
]),
new CustomMethods("DemoAllTypes", "演示四种参数控件", [
new Parameter("text", ParameterType.String, "文本"),
new Parameter("count", ParameterType.Number, "数字"),
new Parameter("enabled", ParameterType.Boolean, "开关"),
new Parameter("character", ParameterType.Enum, "角色", "Characters"),
new Parameter("color", ParameterType.Enum, "颜色"),
]),
];
// 合并外置数据(script/custommethod-data.js),同名以内置为准 // ---------------------------------------------------------------------
// 外置数据 hydrate(script/custommethod-data.js)
// ---------------------------------------------------------------------
function hydrateMethod(raw: any): Method { function hydrateMethod(raw: any): Method {
const shortDescription = raw.shortDescription ?? raw.description ?? "";
const description = raw.description ?? raw.shortDescription ?? "";
const parameters: Parameter[] = Array.isArray(raw.parameters) const parameters: Parameter[] = Array.isArray(raw.parameters)
? raw.parameters.map((item: any) => new Parameter( ? raw.parameters.map((item: any) => new Parameter(
item.name, item.name,
@@ -186,17 +169,19 @@ class NumericRange {
)) ))
: []; : [];
switch (raw.type) { switch (raw.type) {
case MethodType.Mod: return new Mod(raw.name, raw.description ?? ""); case MethodType.Mod: return new Mod(raw.name, shortDescription, description);
case MethodType.Surgery: return new Surgery(raw.name, raw.description ?? ""); case MethodType.Surgery: return new Surgery(raw.name, shortDescription, description);
case MethodType.Comment: return new CommentMethod(raw.name, raw.description ?? "", parameters); case MethodType.Comment: return new CommentMethod(raw.name, shortDescription, description, parameters);
case MethodType.Ink: return new InkMethod(raw.name, raw.description ?? "", parameters); case MethodType.Ink: return new InkMethod(raw.name, shortDescription, description, parameters);
case MethodType.Variable: return new VariableMethod(raw.name, raw.description ?? "", parameters); case MethodType.Variable: return new VariableMethod(raw.name, shortDescription, description, parameters);
case MethodType.Condition: return new ConditionMethod(raw.name, raw.description ?? "", parameters); case MethodType.Condition: return new ConditionMethod(raw.name, shortDescription, description, parameters);
case MethodType.CustomClass: return new CustomClassMethod(raw.name, raw.description ?? ""); case MethodType.CustomClass: return new CustomClassMethod(raw.name, shortDescription, description);
default: return new CustomMethods(raw.name, raw.description ?? "", parameters); default: return new CustomMethods(raw.name, shortDescription, description, parameters);
} }
} }
let methods: Method[] = [];
function loadExternalData(): void { function loadExternalData(): void {
const data = (window as any).CustomMethodSearchData; const data = (window as any).CustomMethodSearchData;
if (!data) return; if (!data) return;
@@ -254,7 +239,7 @@ class NumericRange {
} }
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
// DOM 构建 // DOM 工具
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
function el<K extends keyof HTMLElementTagNameMap>(tag: K, className?: string): HTMLElementTagNameMap[K] { function el<K extends keyof HTMLElementTagNameMap>(tag: K, className?: string): HTMLElementTagNameMap[K] {
const node = document.createElement(tag); const node = document.createElement(tag);
@@ -263,30 +248,18 @@ class NumericRange {
} }
function resolveEnumOptions(parameter: Parameter): string[] { function resolveEnumOptions(parameter: Parameter): string[] {
if (parameter.enumType && enumTypeMap[parameter.enumType]) { if (parameter.enumType && enumTypeMap[parameter.enumType]) return enumTypeMap[parameter.enumType];
return enumTypeMap[parameter.enumType]; if (enumTypeMap[parameter.description]) return enumTypeMap[parameter.description];
} if (enumTypeMap[parameter.name]) return enumTypeMap[parameter.name];
// 兼容把枚举类型名写在 description 的情况
if (enumTypeMap[parameter.description]) {
return enumTypeMap[parameter.description];
}
if (enumTypeMap[parameter.name]) {
return enumTypeMap[parameter.name];
}
return []; return [];
} }
// 布尔值可用 true/false,也可用布尔变量 b0~b9 // 布尔可用 true/false,或布尔变量 b0~b9
const BOOLEAN_OPTIONS: string[] = ["true", "false"]; const BOOLEAN_OPTIONS: string[] = ["true", "false"];
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
BOOLEAN_OPTIONS.push("b" + i); BOOLEAN_OPTIONS.push("b" + i);
} }
function isNumberVariable(value: string): boolean {
// 数字位置允许的变量:i0~i9(整型)、f0~f9(浮点)、b0~b9(布尔)
return /^[ifb][0-9]$/.test(value);
}
function makeCombo(parameter: Parameter, options: string[], onChange: () => void): HTMLElement { function makeCombo(parameter: Parameter, options: string[], onChange: () => void): HTMLElement {
const combo = el("span", "cm-combo"); const combo = el("span", "cm-combo");
const input = el("input", "cm-input cm-combo-input") as HTMLInputElement; const input = el("input", "cm-input cm-combo-input") as HTMLInputElement;
@@ -298,10 +271,7 @@ class NumericRange {
function populate(): void { function populate(): void {
list.textContent = ""; list.textContent = "";
if (options.length === 0) { if (options.length === 0) { list.hidden = true; return; }
list.hidden = true;
return;
}
const query = input.value.trim(); const query = input.value.trim();
const ql = query.toLowerCase(); const ql = query.toLowerCase();
const maxDistance = Math.max(1, Math.floor(query.length / 2)); const maxDistance = Math.max(1, Math.floor(query.length / 2));
@@ -311,10 +281,7 @@ class NumericRange {
editDistance(option, query, false) <= maxDistance) editDistance(option, query, false) <= maxDistance)
: options : options
).slice(0, 10); ).slice(0, 10);
if (shown.length === 0) { if (shown.length === 0) { list.hidden = true; return; }
list.hidden = true;
return;
}
for (const option of shown) { for (const option of shown) {
const item = el("li", "cm-combo-item"); const item = el("li", "cm-combo-item");
item.textContent = option; item.textContent = option;
@@ -330,122 +297,191 @@ class NumericRange {
list.hidden = false; list.hidden = false;
} }
input.addEventListener("input", () => { input.addEventListener("input", () => { populate(); onChange(); });
populate();
onChange();
});
input.addEventListener("focus", populate); input.addEventListener("focus", populate);
input.addEventListener("blur", () => { input.addEventListener("blur", () => { window.setTimeout(() => { list.hidden = true; }, 120); });
window.setTimeout(() => { list.hidden = true; }, 120);
});
combo.appendChild(input); combo.appendChild(input);
combo.appendChild(list); combo.appendChild(list);
return combo; return combo;
} }
function makeControl(parameter: Parameter, onChange: () => void): HTMLElement { function makeModeToggle(initial: ValueMode, onChange: () => void): { el: HTMLButtonElement; get: () => ValueMode } {
switch (parameter.type) { const button = el("button", "cm-mode") as HTMLButtonElement;
case ParameterType.Boolean: { button.type = "button";
return makeCombo(parameter, BOOLEAN_OPTIONS, onChange); let mode = initial;
} const render = (): void => {
case ParameterType.Enum: { button.textContent = mode === "expression" ? "表达式" : "常量";
// 可输入的下拉框:输入时用编辑距离筛选候选项 button.title = "切换 常量 / 表达式";
return makeCombo(parameter, resolveEnumOptions(parameter), onChange); button.classList.toggle("is-expression", mode === "expression");
} };
case ParameterType.Number: { render();
// 普通输入框,合法性由 readParameter 校验(非数字标红) button.addEventListener("click", (event) => {
const number = el("input", "cm-input") as HTMLInputElement; event.preventDefault();
number.type = "text"; mode = mode === "expression" ? "constant" : "expression";
number.inputMode = "decimal"; render();
number.placeholder = parameter.name; onChange();
number.addEventListener("input", onChange); });
return number; return { el: button, get: () => mode };
}
default: {
const text = el("input", "cm-input") as HTMLInputElement;
text.type = "text";
text.placeholder = parameter.name;
text.addEventListener("input", onChange);
return text;
}
}
} }
interface ParameterState { interface ParamState {
text: string; text: string;
valid: boolean; valid: boolean;
invalid: boolean; invalid: boolean;
control: HTMLElement; }
interface Field {
el: HTMLElement;
read: () => ParamState;
} }
function readParameter(parameter: Parameter, control: HTMLElement): ParameterState { function stringValue(value: string): string {
if (value.length === 0) return "<文本>";
return strategies.string === "quote" ? `"${value}"` : `str:${value}`;
}
function makeField(parameter: Parameter, onChange: () => void): Field {
switch (parameter.type) { switch (parameter.type) {
case ParameterType.Boolean: { case ParameterType.Boolean: {
const input = control.querySelector("input") as HTMLInputElement; const wrap = el("span", "cm-field");
const value = input.value.trim(); const checkbox = el("input", "cm-input cm-check") as HTMLInputElement;
const valid = value === "true" || value === "false" || /^b[0-9]$/.test(value); checkbox.type = "checkbox";
return { text: value || "<布尔>", valid, invalid: value.length > 0 && !valid, control }; checkbox.addEventListener("change", onChange);
const combo = makeCombo(parameter, BOOLEAN_OPTIONS, onChange);
const comboInput = combo.querySelector("input") as HTMLInputElement;
let mode: ValueMode = strategies.boolean;
const toggle = makeModeToggle(strategies.boolean, () => {
mode = toggle.get();
applyMode();
onChange();
});
function applyMode(): void {
const expression = mode === "expression";
checkbox.hidden = expression;
combo.hidden = !expression;
} }
case ParameterType.Enum: { applyMode();
const input = control.querySelector("input") as HTMLInputElement; wrap.appendChild(toggle.el);
const value = input.value.trim(); wrap.appendChild(checkbox);
const options = resolveEnumOptions(parameter); wrap.appendChild(combo);
const valid = options.length > 0 return {
? options.some((option) => option.toLowerCase() === value.toLowerCase()) el: wrap,
: value.length > 0; read: () => {
return { text: value || "<选项>", valid, invalid: value.length > 0 && !valid, control }; if (mode === "constant") {
return { text: checkbox.checked ? "true" : "false", valid: true, invalid: false };
}
const value = comboInput.value.trim();
return value.length > 0
? { text: value, valid: true, invalid: false }
: { text: "<布尔>", valid: false, invalid: true };
},
};
} }
case ParameterType.Number: { case ParameterType.Number: {
const input = control as HTMLInputElement; const wrap = el("span", "cm-field");
const input = el("input", "cm-input") as HTMLInputElement;
input.type = "text";
input.inputMode = "decimal";
input.placeholder = parameter.name;
input.addEventListener("input", onChange);
const mode = makeModeToggle(strategies.number, onChange);
wrap.appendChild(mode.el);
wrap.appendChild(input);
return {
el: wrap,
read: () => {
const value = input.value.trim(); const value = input.value.trim();
if (value.length === 0) { if (value.length === 0) return { text: "<数字>", valid: false, invalid: true };
return { text: "<数字>", valid: false, invalid: false, control }; if (mode.get() === "expression") return { text: value, valid: true, invalid: false };
}
if (isNumberVariable(value)) {
return { text: value, valid: true, invalid: false, control };
}
const number = Number(value); const number = Number(value);
let valid = isFinite(number); let valid = isFinite(number);
const range = parameter.numericRange; const range = parameter.numericRange;
if (valid && range) { if (valid && range) {
if (range.isInteger && !Number.isInteger(number)) valid = false; if (range.isInteger && !Number.isInteger(number)) valid = false;
if (number < range.min || number > range.max) valid = false; if (range.min !== undefined && number < range.min) valid = false;
if (range.max !== undefined && number > range.max) valid = false;
} }
return { text: value, valid, invalid: !valid, control }; return { text: value, valid, invalid: !valid };
},
};
}
case ParameterType.Enum: {
const wrap = el("span", "cm-field cm-field-string");
const open = el("span", "cm-lit");
const close = el("span", "cm-lit");
open.textContent = strategies.string === "quote" ? "\"" : "str:";
close.textContent = strategies.string === "quote" ? "\"" : "";
const combo = makeCombo(parameter, resolveEnumOptions(parameter), onChange);
const input = combo.querySelector("input") as HTMLInputElement;
wrap.appendChild(open);
wrap.appendChild(combo);
wrap.appendChild(close);
return {
el: wrap,
read: () => {
const value = input.value.trim();
const options = resolveEnumOptions(parameter);
const valid = options.length > 0
? options.some((option) => option.toLowerCase() === value.toLowerCase())
: value.length > 0;
return { text: stringValue(value), valid, invalid: !valid };
},
};
} }
default: { default: {
const input = control as HTMLInputElement; const wrap = el("span", "cm-field cm-field-string");
const open = el("span", "cm-lit");
const close = el("span", "cm-lit");
open.textContent = strategies.string === "quote" ? "\"" : "str:";
close.textContent = strategies.string === "quote" ? "\"" : "";
const input = el("input", "cm-input") as HTMLInputElement;
input.type = "text";
input.placeholder = parameter.name;
input.addEventListener("input", onChange);
wrap.appendChild(open);
wrap.appendChild(input);
wrap.appendChild(close);
return {
el: wrap,
read: () => {
const value = input.value.trim(); const value = input.value.trim();
return { text: value ? `str:${value}` : "<文本>", valid: value.length > 0, invalid: false, control }; return { text: stringValue(value), valid: value.length > 0, invalid: value.length === 0 };
},
};
} }
} }
} }
function stripStringPrefix(text: string): string { interface Skeleton {
return text.startsWith("str:") ? text.slice(4) : text; prefix: string;
suffix: string;
} }
function formatMethod(method: Method, states: ParameterState[]): string { function methodSkeleton(method: Method, hasParams: boolean): Skeleton {
const values = states.map((state) => state.text);
const args = values.join(", ");
switch (method.type) { switch (method.type) {
case MethodType.Comment: case MethodType.Comment: return { prefix: `()=>${method.name}(`, suffix: ")" };
return `()=>${method.name}(${args})`; case MethodType.Ink: return { prefix: `DOTHIS{${method.name}(`, suffix: ")}" };
case MethodType.Ink: case MethodType.Variable: return { prefix: `${method.name} = `, suffix: "" };
return `DOTHIS{${method.name}(${args})}`; case MethodType.Mod: return { prefix: `"mods": [..., "`, suffix: `"]` };
case MethodType.Variable: case MethodType.CustomClass: return { prefix: `"customClass": "`, suffix: `"` };
return `${method.name} = ${values.length ? stripStringPrefix(values[0]) : ""}`;
case MethodType.Mod:
return `"mods": [..., "${method.name}"]`;
case MethodType.CustomClass:
return `"customClass": "${method.name}"`;
case MethodType.Condition: case MethodType.Condition:
return values.length ? `${method.name}(${args})` : method.name; return hasParams ? { prefix: `${method.name}(`, suffix: ")" } : { prefix: method.name, suffix: "" };
case MethodType.Surgery: case MethodType.Surgery: return { prefix: method.name, suffix: "" };
return method.name;
default: default:
return values.length ? `${method.name}(${args})` : method.name; return hasParams ? { prefix: `${method.name}(`, suffix: ")" } : { prefix: method.name, suffix: "" };
}
}
function formatMethod(method: Method, states: ParamState[]): string {
const args = states.map((state) => state.text).join(", ");
switch (method.type) {
case MethodType.Comment: return `()=>${method.name}(${args})`;
case MethodType.Ink: return `DOTHIS{${method.name}(${args})}`;
case MethodType.Variable: return `${method.name} = ${states.length ? states[0].text : ""}`;
case MethodType.Mod: return `"mods": [..., "${method.name}"]`;
case MethodType.CustomClass: return `"customClass": "${method.name}"`;
case MethodType.Condition: return states.length ? `${method.name}(${args})` : method.name;
case MethodType.Surgery: return method.name;
default: return states.length ? `${method.name}(${args})` : method.name;
} }
} }
@@ -456,7 +492,7 @@ class NumericRange {
return true; return true;
} }
} catch (error) { } catch (error) {
// fallthrough to execCommand // fall through
} }
try { try {
const area = document.createElement("textarea"); const area = document.createElement("textarea");
@@ -473,20 +509,26 @@ class NumericRange {
} }
} }
// ---------------------------------------------------------------------
// 渲染
// ---------------------------------------------------------------------
function renderContainer(container: HTMLElement): void { function renderContainer(container: HTMLElement): void {
if (container.dataset.cmReady === "1") return; if (container.dataset.cmReady === "1") return;
container.dataset.cmReady = "1"; container.dataset.cmReady = "1";
// 优先使用 custommethod.md 里写好的骨架;缺失时再动态生成 let currentMethod: Method | null = null;
let query = container.querySelector<HTMLInputElement>(".cm-query"); let query = container.querySelector<HTMLInputElement>(".cm-query");
let suggest = container.querySelector<HTMLUListElement>(".cm-suggest"); let suggest = container.querySelector<HTMLUListElement>(".cm-suggest");
let selected = container.querySelector<HTMLElement>(".cm-selected"); let selected = container.querySelector<HTMLElement>(".cm-selected");
let nameEl = container.querySelector<HTMLElement>(".cm-name"); let nameEl = container.querySelector<HTMLElement>(".cm-name");
let descEl = container.querySelector<HTMLElement>(".cm-desc"); let shortEl = container.querySelector<HTMLElement>(".cm-short");
let row = container.querySelector<HTMLElement>(".cm-row"); let call = container.querySelector<HTMLElement>(".cm-call");
let preview = container.querySelector<HTMLElement>(".cm-preview"); let preview = container.querySelector<HTMLElement>(".cm-preview");
let desc = container.querySelector<HTMLElement>(".cm-desc");
let options = container.querySelector<HTMLElement>(".cm-options");
if (!query || !suggest || !selected || !nameEl || !descEl || !row || !preview) { if (!query || !suggest || !selected || !nameEl || !shortEl || !call || !preview || !desc) {
container.textContent = ""; container.textContent = "";
const searchBar = el("div", "cm-search"); const searchBar = el("div", "cm-search");
query = el("input", "cm-query") as HTMLInputElement; query = el("input", "cm-query") as HTMLInputElement;
@@ -500,34 +542,68 @@ class NumericRange {
selected.hidden = true; selected.hidden = true;
const head = el("div", "cm-head"); const head = el("div", "cm-head");
nameEl = el("span", "cm-name"); nameEl = el("span", "cm-name");
descEl = el("span", "cm-desc"); shortEl = el("span", "cm-short");
head.appendChild(nameEl); head.appendChild(nameEl);
head.appendChild(descEl); head.appendChild(shortEl);
row = el("div", "cm-row"); call = el("div", "cm-call");
preview = el("code", "cm-preview"); preview = el("code", "cm-preview");
desc = el("div", "cm-desc");
selected.appendChild(head); selected.appendChild(head);
selected.appendChild(row); selected.appendChild(call);
selected.appendChild(preview); selected.appendChild(preview);
selected.appendChild(desc);
container.appendChild(searchBar); container.appendChild(searchBar);
container.appendChild(selected); container.appendChild(selected);
} }
// 搜索框下的策略选项
options = container.querySelector<HTMLElement>(".cm-options");
if (!options) {
options = el("div", "cm-options");
(query.parentElement as HTMLElement).insertAdjacentElement("afterend", options);
}
if (!options.querySelector(".cm-option-select")) {
const optionsBox = options;
function addOption(label: string, key: "string" | "number" | "boolean", values: [string, string][], current: string): void {
const wrap = el("label", "cm-option");
const text = el("span", "cm-option-label");
text.textContent = label;
const select = el("select", "cm-option-select") as HTMLSelectElement;
for (const [value, caption] of values) {
const opt = el("option");
opt.value = value;
opt.textContent = caption;
select.appendChild(opt);
}
select.value = current;
select.addEventListener("change", () => {
(strategies as any)[key] = select.value;
if (currentMethod) { selectMethod(currentMethod); }
});
wrap.appendChild(text);
wrap.appendChild(select);
optionsBox.appendChild(wrap);
}
addOption("字符串", "string", [["prefix", "str: 前缀"], ["quote", "引号 \"…\""]], strategies.string);
}
const queryEl = query; const queryEl = query;
const suggestEl = suggest; const suggestEl = suggest;
const selectedEl = selected; const selectedEl = selected;
const nameNode = nameEl; const nameNode = nameEl;
const descNode = descEl; const shortNode = shortEl;
const rowEl = row; const callEl = call;
const previewEl = preview; const previewEl = preview;
const descNode = desc;
const copyBtn = el("button", "cm-copy") as HTMLButtonElement; const copyBtn = el("button", "cm-copy") as HTMLButtonElement;
copyBtn.type = "button"; copyBtn.type = "button";
copyBtn.textContent = "复制"; copyBtn.textContent = "复制";
copyBtn.hidden = true; copyBtn.hidden = true;
copyBtn.title = "复制生成的调用代码"; copyBtn.title = "复制生成的代码";
selectedEl.appendChild(copyBtn); selectedEl.appendChild(copyBtn);
copyBtn.addEventListener("click", () => { copyBtn.addEventListener("click", () => {
copyText(previewEl.textContent).then((ok) => { copyText(previewEl.textContent ?? "").then((ok) => {
copyBtn.textContent = ok ? "已复制" : "复制失败"; copyBtn.textContent = ok ? "已复制" : "复制失败";
window.setTimeout(() => { copyBtn.textContent = "复制"; }, 1200); window.setTimeout(() => { copyBtn.textContent = "复制"; }, 1200);
}); });
@@ -538,56 +614,69 @@ class NumericRange {
} }
function selectMethod(method: Method): void { function selectMethod(method: Method): void {
currentMethod = method;
clearSuggest(); clearSuggest();
queryEl.value = method.name; queryEl.value = method.name;
selectedEl.hidden = false; selectedEl.hidden = false;
nameNode.textContent = method.name; nameNode.textContent = method.name;
descNode.textContent = method.description; shortNode.textContent = method.shortDescription || "";
rowEl.textContent = ""; descNode.textContent = method.description || "";
callEl.textContent = "";
previewEl.textContent = ""; previewEl.textContent = "";
copyBtn.hidden = true; copyBtn.hidden = true;
const parameters = parametersOf(method); const parameters = parametersOf(method);
const controls: { parameter: Parameter; control: HTMLElement }[] = []; const fields: Field[] = [];
function updatePreview(): void { function updatePreview(): void {
const states = controls.map((item) => readParameter(item.parameter, item.control)); const states = fields.map((field) => field.read());
states.forEach((state) => state.control.classList.toggle("cm-invalid", state.invalid)); fields.forEach((field, i) => field.el.classList.toggle("cm-invalid", states[i].invalid));
previewEl.textContent = formatMethod(method, states); previewEl.textContent = formatMethod(method, states);
copyBtn.hidden = !states.every((state) => state.valid); copyBtn.hidden = !states.every((state) => state.valid);
} }
const skeleton = methodSkeleton(method, parameters.length > 0);
const code = el("code", "cm-code");
const prefix = el("span", "cm-lit");
prefix.textContent = skeleton.prefix;
code.appendChild(prefix);
parameters.forEach((parameter, index) => {
if (index > 0) {
const sep = el("span", "cm-lit");
sep.textContent = ", ";
code.appendChild(sep);
}
const field = makeField(parameter, updatePreview);
field.el.title = parameter.description;
code.appendChild(field.el);
fields.push(field);
});
if (skeleton.suffix) {
const suffix = el("span", "cm-lit");
suffix.textContent = skeleton.suffix;
code.appendChild(suffix);
}
callEl.appendChild(code);
if (parameters.length === 0) { if (parameters.length === 0) {
const empty = el("span", "cm-empty"); const empty = el("span", "cm-empty");
empty.textContent = "该方法没有参数"; empty.textContent = "(无参数)";
rowEl.appendChild(empty); callEl.appendChild(empty);
updatePreview();
return;
} }
for (const parameter of parameters) {
const field = el("label", "cm-param");
field.title = parameter.description;
const label = el("span", "cm-param-name");
label.textContent = parameter.name;
const control = makeControl(parameter, updatePreview);
field.appendChild(label);
field.appendChild(control);
rowEl.appendChild(field);
controls.push({ parameter, control });
}
updatePreview(); updatePreview();
} }
function updateSuggestions(): void { function updateSuggestions(): void {
clearSuggest(); clearSuggest();
const matches = searchMethods(queryEl.value); for (const method of searchMethods(queryEl.value)) {
for (const method of matches) {
const item = el("li", "cm-suggest-item"); const item = el("li", "cm-suggest-item");
const nameSpan = el("span", "cm-suggest-name"); const nameSpan = el("span", "cm-suggest-name");
nameSpan.textContent = method.name; nameSpan.textContent = method.name;
const descSpan = el("span", "cm-suggest-desc"); const descSpan = el("span", "cm-suggest-desc");
descSpan.textContent = method.description; descSpan.textContent = method.shortDescription || method.description;
item.appendChild(nameSpan); item.appendChild(nameSpan);
item.appendChild(descSpan); item.appendChild(descSpan);
item.addEventListener("mousedown", (event) => { item.addEventListener("mousedown", (event) => {
@@ -613,8 +702,7 @@ class NumericRange {
} }
function initAll(): void { function initAll(): void {
const containers = document.querySelectorAll<HTMLElement>(".custommethods-search"); document.querySelectorAll<HTMLElement>(".custommethods-search").forEach((container) => renderContainer(container));
containers.forEach((container) => renderContainer(container));
} }
function register(): void { function register(): void {
@@ -634,11 +722,9 @@ class NumericRange {
search: searchMethods, search: searchMethods,
editDistance, editDistance,
renderAll: initAll, renderAll: initAll,
strategies,
getMethods: () => methods, getMethods: () => methods,
setMethods: (list: Method[]) => { setMethods: (list: Method[]) => { methods = list; initAll(); },
methods = list;
initAll();
},
Parameter, Parameter,
ParameterType, ParameterType,
Method, Method,
+92
View File
@@ -485,4 +485,96 @@ div.custommethods-search {
border-color: #3b8e59; border-color: #3b8e59;
} }
} }
& .cm-options {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 6px;
font-size: 12px;
}
& .cm-option {
display: inline-flex;
align-items: center;
gap: 4px;
opacity: 0.85;
}
& .cm-option-select {
padding: 2px 4px;
font-size: 12px;
color: inherit;
background-color: var(--background-color);
border: 1px solid var(--background-border-color);
border-radius: 4px;
}
& .cm-short {
font-size: 12px;
opacity: 0.7;
}
& .cm-call {
margin-top: 8px;
}
& .cm-code {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
gap: 4px;
padding: 8px 10px;
font-family: d9c, d9, monospace;
background-color: rgba(127, 127, 127, 0.12);
border-radius: 6px;
}
& .cm-lit {
font-family: d9c, d9, monospace;
opacity: 0.85;
white-space: pre;
}
& .cm-field {
display: inline-flex;
align-items: center;
gap: 3px;
& [hidden] {
display: none !important;
}
}
& .cm-field.cm-invalid .cm-input {
border-color: #e5484d;
box-shadow: 0 0 0 1px #e5484d;
}
& .cm-check {
width: auto;
}
& .cm-desc {
margin-top: 8px;
font-size: 12px;
line-height: 1.5;
opacity: 0.75;
white-space: pre-wrap;
}
& .cm-mode {
padding: 1px 6px;
font-size: 11px;
color: inherit;
background-color: transparent;
border: 1px solid var(--background-border-color);
border-radius: 8px;
cursor: pointer;
&.is-expression {
color: #3b8e59;
border-color: #3b8e59;
}
}
} }