Compare commits
1
Commits
check
..
19142332e3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19142332e3 |
@@ -97,15 +97,13 @@
|
|||||||
<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-short"></span>
|
<span class="cm-desc"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cm-call"></div>
|
<div class="cm-row"></div>
|
||||||
<code class="cm-preview"></code>
|
<code class="cm-preview"></code>
|
||||||
<div class="cm-desc"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+622
-1149
File diff suppressed because it is too large
Load Diff
+190
-275
@@ -50,56 +50,55 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
class Method {
|
class Method {
|
||||||
constructor(name, type, shortDescription, description) {
|
constructor(name, type, 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, shortDescription, description) {
|
constructor(name, description) {
|
||||||
super(name, MethodType.Mod, shortDescription, description);
|
super(name, MethodType.Mod, description);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class Surgery extends Method {
|
class Surgery extends Method {
|
||||||
constructor(name, shortDescription, description) {
|
constructor(name, description) {
|
||||||
super(name, MethodType.Surgery, shortDescription, description);
|
super(name, MethodType.Surgery, description);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class CommentMethod extends Method {
|
class CommentMethod extends Method {
|
||||||
constructor(name, shortDescription, description, parameters) {
|
constructor(name, description, parameters) {
|
||||||
super(name, MethodType.Comment, shortDescription, description);
|
super(name, MethodType.Comment, description);
|
||||||
this.parameters = parameters;
|
this.parameters = parameters;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class InkMethod extends Method {
|
class InkMethod extends Method {
|
||||||
constructor(name, shortDescription, description, parameters) {
|
constructor(name, description, parameters) {
|
||||||
super(name, MethodType.Ink, shortDescription, description);
|
super(name, MethodType.Ink, description);
|
||||||
this.parameters = parameters;
|
this.parameters = parameters;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class CustomMethods extends Method {
|
class CustomMethods extends Method {
|
||||||
constructor(name, shortDescription, description, parameters) {
|
constructor(name, description, parameters) {
|
||||||
super(name, MethodType.CustomMethod, shortDescription, description);
|
super(name, MethodType.CustomMethod, description);
|
||||||
this.parameters = parameters;
|
this.parameters = parameters;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class VariableMethod extends Method {
|
class VariableMethod extends Method {
|
||||||
constructor(name, shortDescription, description, parameters) {
|
constructor(name, description, parameters) {
|
||||||
super(name, MethodType.Variable, shortDescription, description);
|
super(name, MethodType.Variable, description);
|
||||||
this.parameters = parameters;
|
this.parameters = parameters;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class ConditionMethod extends Method {
|
class ConditionMethod extends Method {
|
||||||
constructor(name, shortDescription, description, parameters) {
|
constructor(name, description, parameters) {
|
||||||
super(name, MethodType.Condition, shortDescription, description);
|
super(name, MethodType.Condition, description);
|
||||||
this.parameters = parameters;
|
this.parameters = parameters;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class CustomClassMethod extends Method {
|
class CustomClassMethod extends Method {
|
||||||
constructor(name, shortDescription, description) {
|
constructor(name, description) {
|
||||||
super(name, MethodType.CustomClass, shortDescription, description);
|
super(name, MethodType.CustomClass, description);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function editDistance(a, b, caseSensitive = true) {
|
function editDistance(a, b, caseSensitive = true) {
|
||||||
@@ -132,19 +131,41 @@ 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",
|
|
||||||
};
|
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
// 外置数据 hydrate(script/custommethod-data.js)
|
// 数据:可直接在此编辑,或通过 window.CustomMethodSearch.setMethods(...) 覆盖
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
|
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;
|
var _a, _b, _c, _d, _e, _f, _g, _h;
|
||||||
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;
|
||||||
@@ -154,17 +175,16 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
switch (raw.type) {
|
switch (raw.type) {
|
||||||
case MethodType.Mod: return new Mod(raw.name, shortDescription, description);
|
case MethodType.Mod: return new Mod(raw.name, (_a = raw.description) !== null && _a !== void 0 ? _a : "");
|
||||||
case MethodType.Surgery: return new Surgery(raw.name, shortDescription, description);
|
case MethodType.Surgery: return new Surgery(raw.name, (_b = raw.description) !== null && _b !== void 0 ? _b : "");
|
||||||
case MethodType.Comment: return new CommentMethod(raw.name, shortDescription, description, parameters);
|
case MethodType.Comment: return new CommentMethod(raw.name, (_c = raw.description) !== null && _c !== void 0 ? _c : "", parameters);
|
||||||
case MethodType.Ink: return new InkMethod(raw.name, shortDescription, description, parameters);
|
case MethodType.Ink: return new InkMethod(raw.name, (_d = raw.description) !== null && _d !== void 0 ? _d : "", parameters);
|
||||||
case MethodType.Variable: return new VariableMethod(raw.name, shortDescription, description, parameters);
|
case MethodType.Variable: return new VariableMethod(raw.name, (_e = raw.description) !== null && _e !== void 0 ? _e : "", parameters);
|
||||||
case MethodType.Condition: return new ConditionMethod(raw.name, shortDescription, description, parameters);
|
case MethodType.Condition: return new ConditionMethod(raw.name, (_f = raw.description) !== null && _f !== void 0 ? _f : "", parameters);
|
||||||
case MethodType.CustomClass: return new CustomClassMethod(raw.name, shortDescription, description);
|
case MethodType.CustomClass: return new CustomClassMethod(raw.name, (_g = raw.description) !== null && _g !== void 0 ? _g : "");
|
||||||
default: return new CustomMethods(raw.name, shortDescription, description, parameters);
|
default: return new CustomMethods(raw.name, (_h = raw.description) !== null && _h !== void 0 ? _h : "", parameters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let methods = [];
|
|
||||||
function loadExternalData() {
|
function loadExternalData() {
|
||||||
const data = window.CustomMethodSearchData;
|
const data = window.CustomMethodSearchData;
|
||||||
if (!data)
|
if (!data)
|
||||||
@@ -227,7 +247,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);
|
||||||
@@ -236,19 +256,27 @@ 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");
|
||||||
@@ -288,177 +316,112 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|||||||
}
|
}
|
||||||
list.hidden = false;
|
list.hidden = false;
|
||||||
}
|
}
|
||||||
input.addEventListener("input", () => { populate(); onChange(); });
|
input.addEventListener("input", () => {
|
||||||
|
populate();
|
||||||
|
onChange();
|
||||||
|
});
|
||||||
input.addEventListener("focus", populate);
|
input.addEventListener("focus", populate);
|
||||||
input.addEventListener("blur", () => { window.setTimeout(() => { list.hidden = true; }, 120); });
|
input.addEventListener("blur", () => {
|
||||||
|
window.setTimeout(() => { list.hidden = true; }, 120);
|
||||||
|
});
|
||||||
combo.appendChild(input);
|
combo.appendChild(input);
|
||||||
combo.appendChild(list);
|
combo.appendChild(list);
|
||||||
return combo;
|
return combo;
|
||||||
}
|
}
|
||||||
function makeModeToggle(initial, onChange) {
|
function makeControl(parameter, 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: {
|
||||||
const wrap = el("span", "cm-field");
|
return makeCombo(parameter, BOOLEAN_OPTIONS, onChange);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
applyMode();
|
case ParameterType.Enum: {
|
||||||
wrap.appendChild(toggle.el);
|
// 可输入的下拉框:输入时用编辑距离筛选候选项
|
||||||
wrap.appendChild(checkbox);
|
return makeCombo(parameter, resolveEnumOptions(parameter), onChange);
|
||||||
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: {
|
||||||
const wrap = el("span", "cm-field");
|
// 普通输入框,合法性由 readParameter 校验(非数字标红)
|
||||||
const input = el("input", "cm-input");
|
const number = el("input", "cm-input");
|
||||||
input.type = "text";
|
number.type = "text";
|
||||||
input.inputMode = "decimal";
|
number.inputMode = "decimal";
|
||||||
input.placeholder = parameter.name;
|
number.placeholder = parameter.name;
|
||||||
input.addEventListener("input", onChange);
|
number.addEventListener("input", onChange);
|
||||||
const mode = makeModeToggle(strategies.number, onChange);
|
return number;
|
||||||
wrap.appendChild(mode.el);
|
}
|
||||||
wrap.appendChild(input);
|
default: {
|
||||||
return {
|
const text = el("input", "cm-input");
|
||||||
el: wrap,
|
text.type = "text";
|
||||||
read: () => {
|
text.placeholder = parameter.name;
|
||||||
|
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();
|
||||||
if (value.length === 0)
|
const valid = value === "true" || value === "false" || /^b[0-9]$/.test(value);
|
||||||
return { text: "<数字>", valid: false, invalid: true };
|
return { text: value || "<布尔>", valid, invalid: value.length > 0 && !valid, control };
|
||||||
if (mode.get() === "expression")
|
}
|
||||||
return { text: value, valid: true, invalid: false };
|
case ParameterType.Enum: {
|
||||||
|
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 (range.min !== undefined && number < range.min)
|
if (number < range.min || number > range.max)
|
||||||
valid = false;
|
|
||||||
if (range.max !== undefined && number > range.max)
|
|
||||||
valid = false;
|
valid = false;
|
||||||
}
|
}
|
||||||
return { text: value, valid, invalid: !valid };
|
return { text: value, valid, invalid: !valid, control };
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
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 wrap = el("span", "cm-field cm-field-string");
|
const input = control;
|
||||||
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: stringValue(value), valid: value.length > 0, invalid: value.length === 0 };
|
return { text: value ? `str:${value}` : "<文本>", valid: value.length > 0, invalid: false, control };
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function methodSkeleton(method, hasParams) {
|
function stripStringPrefix(text) {
|
||||||
switch (method.type) {
|
return text.startsWith("str:") ? text.slice(4) : text;
|
||||||
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 args = states.map((state) => state.text).join(", ");
|
const values = states.map((state) => state.text);
|
||||||
|
const args = values.join(", ");
|
||||||
switch (method.type) {
|
switch (method.type) {
|
||||||
case MethodType.Comment: return `()=>${method.name}(${args})`;
|
case MethodType.Comment:
|
||||||
case MethodType.Ink: return `DOTHIS{${method.name}(${args})}`;
|
return `()=>${method.name}(${args})`;
|
||||||
case MethodType.Variable: return `${method.name} = ${states.length ? states[0].text : ""}`;
|
case MethodType.Ink:
|
||||||
case MethodType.Mod: return `"mods": [..., "${method.name}"]`;
|
return `DOTHIS{${method.name}(${args})}`;
|
||||||
case MethodType.CustomClass: return `"customClass": "${method.name}"`;
|
case MethodType.Variable:
|
||||||
case MethodType.Condition: return states.length ? `${method.name}(${args})` : method.name;
|
return `${method.name} = ${values.length ? stripStringPrefix(values[0]) : ""}`;
|
||||||
case MethodType.Surgery: return method.name;
|
case MethodType.Mod:
|
||||||
default: return states.length ? `${method.name}(${args})` : method.name;
|
return `"mods": [..., "${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) {
|
||||||
@@ -470,7 +433,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
// fall through
|
// fallthrough to execCommand
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const area = document.createElement("textarea");
|
const area = document.createElement("textarea");
|
||||||
@@ -488,24 +451,19 @@ 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";
|
||||||
let currentMethod = null;
|
// 优先使用 custommethod.md 里写好的骨架;缺失时再动态生成
|
||||||
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 shortEl = container.querySelector(".cm-short");
|
let descEl = container.querySelector(".cm-desc");
|
||||||
let call = container.querySelector(".cm-call");
|
let row = container.querySelector(".cm-row");
|
||||||
let preview = container.querySelector(".cm-preview");
|
let preview = container.querySelector(".cm-preview");
|
||||||
let desc = container.querySelector(".cm-desc");
|
if (!query || !suggest || !selected || !nameEl || !descEl || !row || !preview) {
|
||||||
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");
|
||||||
@@ -519,68 +477,32 @@ 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");
|
||||||
shortEl = el("span", "cm-short");
|
descEl = el("span", "cm-desc");
|
||||||
head.appendChild(nameEl);
|
head.appendChild(nameEl);
|
||||||
head.appendChild(shortEl);
|
head.appendChild(descEl);
|
||||||
call = el("div", "cm-call");
|
row = el("div", "cm-row");
|
||||||
preview = el("code", "cm-preview");
|
preview = el("code", "cm-preview");
|
||||||
desc = el("div", "cm-desc");
|
|
||||||
selected.appendChild(head);
|
selected.appendChild(head);
|
||||||
selected.appendChild(call);
|
selected.appendChild(row);
|
||||||
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 shortNode = shortEl;
|
const descNode = descEl;
|
||||||
const callEl = call;
|
const rowEl = row;
|
||||||
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", () => {
|
||||||
var _a;
|
copyText(previewEl.textContent).then((ok) => {
|
||||||
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);
|
||||||
});
|
});
|
||||||
@@ -589,61 +511,51 @@ 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;
|
||||||
shortNode.textContent = method.shortDescription || "";
|
descNode.textContent = method.description;
|
||||||
descNode.textContent = method.description || "";
|
rowEl.textContent = "";
|
||||||
callEl.textContent = "";
|
|
||||||
previewEl.textContent = "";
|
previewEl.textContent = "";
|
||||||
copyBtn.hidden = true;
|
copyBtn.hidden = true;
|
||||||
const parameters = parametersOf(method);
|
const parameters = parametersOf(method);
|
||||||
const fields = [];
|
const controls = [];
|
||||||
function updatePreview() {
|
function updatePreview() {
|
||||||
const states = fields.map((field) => field.read());
|
const states = controls.map((item) => readParameter(item.parameter, item.control));
|
||||||
fields.forEach((field, i) => field.el.classList.toggle("cm-invalid", states[i].invalid));
|
states.forEach((state) => state.control.classList.toggle("cm-invalid", state.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 = "该方法没有参数";
|
||||||
callEl.appendChild(empty);
|
rowEl.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();
|
||||||
for (const method of searchMethods(queryEl.value)) {
|
const matches = 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.shortDescription || method.description;
|
descSpan.textContent = method.description;
|
||||||
item.appendChild(nameSpan);
|
item.appendChild(nameSpan);
|
||||||
item.appendChild(descSpan);
|
item.appendChild(descSpan);
|
||||||
item.addEventListener("mousedown", (event) => {
|
item.addEventListener("mousedown", (event) => {
|
||||||
@@ -666,7 +578,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
function initAll() {
|
function initAll() {
|
||||||
document.querySelectorAll(".custommethods-search").forEach((container) => renderContainer(container));
|
const containers = document.querySelectorAll(".custommethods-search");
|
||||||
|
containers.forEach((container) => renderContainer(container));
|
||||||
}
|
}
|
||||||
function register() {
|
function register() {
|
||||||
const docsify = window.$docsify;
|
const docsify = window.$docsify;
|
||||||
@@ -686,9 +599,11 @@ 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) => { methods = list; initAll(); },
|
setMethods: (list) => {
|
||||||
|
methods = list;
|
||||||
|
initAll();
|
||||||
|
},
|
||||||
Parameter,
|
Parameter,
|
||||||
ParameterType,
|
ParameterType,
|
||||||
Method,
|
Method,
|
||||||
|
|||||||
+209
-295
@@ -17,20 +17,17 @@
|
|||||||
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;
|
||||||
@@ -53,63 +50,61 @@
|
|||||||
class Method {
|
class Method {
|
||||||
name: string;
|
name: string;
|
||||||
type: MethodType;
|
type: MethodType;
|
||||||
shortDescription: string;
|
|
||||||
description: string;
|
description: string;
|
||||||
public constructor(name: string, type: MethodType, shortDescription: string, description: string) {
|
public constructor(name: string, type: MethodType, 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, shortDescription: string, description: string) {
|
public constructor(name: string, description: string) {
|
||||||
super(name, MethodType.Mod, shortDescription, description);
|
super(name, MethodType.Mod, description);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class Surgery extends Method {
|
class Surgery extends Method {
|
||||||
public constructor(name: string, shortDescription: string, description: string) {
|
public constructor(name: string, description: string) {
|
||||||
super(name, MethodType.Surgery, shortDescription, description);
|
super(name, MethodType.Surgery, description);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class CommentMethod extends Method {
|
class CommentMethod extends Method {
|
||||||
parameters: Parameter[];
|
parameters: Parameter[];
|
||||||
public constructor(name: string, shortDescription: string, description: string, parameters: Parameter[]) {
|
public constructor(name: string, description: string, parameters: Parameter[]) {
|
||||||
super(name, MethodType.Comment, shortDescription, description);
|
super(name, MethodType.Comment, description);
|
||||||
this.parameters = parameters;
|
this.parameters = parameters;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class InkMethod extends Method {
|
class InkMethod extends Method {
|
||||||
parameters: Parameter[];
|
parameters: Parameter[];
|
||||||
public constructor(name: string, shortDescription: string, description: string, parameters: Parameter[]) {
|
public constructor(name: string, description: string, parameters: Parameter[]) {
|
||||||
super(name, MethodType.Ink, shortDescription, description);
|
super(name, MethodType.Ink, description);
|
||||||
this.parameters = parameters;
|
this.parameters = parameters;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class CustomMethods extends Method {
|
class CustomMethods extends Method {
|
||||||
parameters: Parameter[];
|
parameters: Parameter[];
|
||||||
public constructor(name: string, shortDescription: string, description: string, parameters: Parameter[]) {
|
public constructor(name: string, description: string, parameters: Parameter[]) {
|
||||||
super(name, MethodType.CustomMethod, shortDescription, description);
|
super(name, MethodType.CustomMethod, description);
|
||||||
this.parameters = parameters;
|
this.parameters = parameters;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class VariableMethod extends Method {
|
class VariableMethod extends Method {
|
||||||
parameters: Parameter[];
|
parameters: Parameter[];
|
||||||
public constructor(name: string, shortDescription: string, description: string, parameters: Parameter[]) {
|
public constructor(name: string, description: string, parameters: Parameter[]) {
|
||||||
super(name, MethodType.Variable, shortDescription, description);
|
super(name, MethodType.Variable, description);
|
||||||
this.parameters = parameters;
|
this.parameters = parameters;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class ConditionMethod extends Method {
|
class ConditionMethod extends Method {
|
||||||
parameters: Parameter[];
|
parameters: Parameter[];
|
||||||
public constructor(name: string, shortDescription: string, description: string, parameters: Parameter[]) {
|
public constructor(name: string, description: string, parameters: Parameter[]) {
|
||||||
super(name, MethodType.Condition, shortDescription, description);
|
super(name, MethodType.Condition, description);
|
||||||
this.parameters = parameters;
|
this.parameters = parameters;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class CustomClassMethod extends Method {
|
class CustomClassMethod extends Method {
|
||||||
public constructor(name: string, shortDescription: string, description: string) {
|
public constructor(name: string, description: string) {
|
||||||
super(name, MethodType.CustomClass, shortDescription, description);
|
super(name, MethodType.CustomClass, description);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,19 +139,41 @@
|
|||||||
return (method as CommentMethod | InkMethod | CustomMethods | VariableMethod | ConditionMethod).parameters ?? [];
|
return (method as CommentMethod | InkMethod | CustomMethods | VariableMethod | ConditionMethod).parameters ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 全局策略:字符串写法、数字/布尔默认的常量/表达式状态
|
// ---------------------------------------------------------------------
|
||||||
const strategies: { string: StringStrategy; number: ValueMode; boolean: ValueMode } = {
|
// 数据:可直接在此编辑,或通过 window.CustomMethodSearch.setMethods(...) 覆盖
|
||||||
string: "prefix",
|
// ---------------------------------------------------------------------
|
||||||
number: "constant",
|
let methods: Method[] = [
|
||||||
boolean: "constant",
|
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),同名以内置为准
|
||||||
// 外置数据 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,
|
||||||
@@ -169,19 +186,17 @@
|
|||||||
))
|
))
|
||||||
: [];
|
: [];
|
||||||
switch (raw.type) {
|
switch (raw.type) {
|
||||||
case MethodType.Mod: return new Mod(raw.name, shortDescription, description);
|
case MethodType.Mod: return new Mod(raw.name, raw.description ?? "");
|
||||||
case MethodType.Surgery: return new Surgery(raw.name, shortDescription, description);
|
case MethodType.Surgery: return new Surgery(raw.name, raw.description ?? "");
|
||||||
case MethodType.Comment: return new CommentMethod(raw.name, shortDescription, description, parameters);
|
case MethodType.Comment: return new CommentMethod(raw.name, raw.description ?? "", parameters);
|
||||||
case MethodType.Ink: return new InkMethod(raw.name, shortDescription, description, parameters);
|
case MethodType.Ink: return new InkMethod(raw.name, raw.description ?? "", parameters);
|
||||||
case MethodType.Variable: return new VariableMethod(raw.name, shortDescription, description, parameters);
|
case MethodType.Variable: return new VariableMethod(raw.name, raw.description ?? "", parameters);
|
||||||
case MethodType.Condition: return new ConditionMethod(raw.name, shortDescription, description, parameters);
|
case MethodType.Condition: return new ConditionMethod(raw.name, raw.description ?? "", parameters);
|
||||||
case MethodType.CustomClass: return new CustomClassMethod(raw.name, shortDescription, description);
|
case MethodType.CustomClass: return new CustomClassMethod(raw.name, raw.description ?? "");
|
||||||
default: return new CustomMethods(raw.name, shortDescription, description, parameters);
|
default: return new CustomMethods(raw.name, raw.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;
|
||||||
@@ -239,7 +254,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
// 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);
|
||||||
@@ -248,18 +263,30 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveEnumOptions(parameter: Parameter): string[] {
|
function resolveEnumOptions(parameter: Parameter): string[] {
|
||||||
if (parameter.enumType && enumTypeMap[parameter.enumType]) return enumTypeMap[parameter.enumType];
|
if (parameter.enumType && enumTypeMap[parameter.enumType]) {
|
||||||
if (enumTypeMap[parameter.description]) return enumTypeMap[parameter.description];
|
return enumTypeMap[parameter.enumType];
|
||||||
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;
|
||||||
@@ -271,7 +298,10 @@
|
|||||||
|
|
||||||
function populate(): void {
|
function populate(): void {
|
||||||
list.textContent = "";
|
list.textContent = "";
|
||||||
if (options.length === 0) { list.hidden = true; return; }
|
if (options.length === 0) {
|
||||||
|
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));
|
||||||
@@ -281,7 +311,10 @@
|
|||||||
editDistance(option, query, false) <= maxDistance)
|
editDistance(option, query, false) <= maxDistance)
|
||||||
: options
|
: options
|
||||||
).slice(0, 10);
|
).slice(0, 10);
|
||||||
if (shown.length === 0) { list.hidden = true; return; }
|
if (shown.length === 0) {
|
||||||
|
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;
|
||||||
@@ -297,191 +330,122 @@
|
|||||||
list.hidden = false;
|
list.hidden = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
input.addEventListener("input", () => { populate(); onChange(); });
|
input.addEventListener("input", () => {
|
||||||
|
populate();
|
||||||
|
onChange();
|
||||||
|
});
|
||||||
input.addEventListener("focus", populate);
|
input.addEventListener("focus", populate);
|
||||||
input.addEventListener("blur", () => { window.setTimeout(() => { list.hidden = true; }, 120); });
|
input.addEventListener("blur", () => {
|
||||||
|
window.setTimeout(() => { list.hidden = true; }, 120);
|
||||||
|
});
|
||||||
|
|
||||||
combo.appendChild(input);
|
combo.appendChild(input);
|
||||||
combo.appendChild(list);
|
combo.appendChild(list);
|
||||||
return combo;
|
return combo;
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeModeToggle(initial: ValueMode, onChange: () => void): { el: HTMLButtonElement; get: () => ValueMode } {
|
function makeControl(parameter: Parameter, onChange: () => void): HTMLElement {
|
||||||
const button = el("button", "cm-mode") as HTMLButtonElement;
|
switch (parameter.type) {
|
||||||
button.type = "button";
|
case ParameterType.Boolean: {
|
||||||
let mode = initial;
|
return makeCombo(parameter, BOOLEAN_OPTIONS, onChange);
|
||||||
const render = (): void => {
|
}
|
||||||
button.textContent = mode === "expression" ? "表达式" : "常量";
|
case ParameterType.Enum: {
|
||||||
button.title = "切换 常量 / 表达式";
|
// 可输入的下拉框:输入时用编辑距离筛选候选项
|
||||||
button.classList.toggle("is-expression", mode === "expression");
|
return makeCombo(parameter, resolveEnumOptions(parameter), onChange);
|
||||||
};
|
}
|
||||||
render();
|
case ParameterType.Number: {
|
||||||
button.addEventListener("click", (event) => {
|
// 普通输入框,合法性由 readParameter 校验(非数字标红)
|
||||||
event.preventDefault();
|
const number = el("input", "cm-input") as HTMLInputElement;
|
||||||
mode = mode === "expression" ? "constant" : "expression";
|
number.type = "text";
|
||||||
render();
|
number.inputMode = "decimal";
|
||||||
onChange();
|
number.placeholder = parameter.name;
|
||||||
});
|
number.addEventListener("input", onChange);
|
||||||
return { el: button, get: () => mode };
|
return number;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
const text = el("input", "cm-input") as HTMLInputElement;
|
||||||
|
text.type = "text";
|
||||||
|
text.placeholder = parameter.name;
|
||||||
|
text.addEventListener("input", onChange);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ParamState {
|
interface ParameterState {
|
||||||
text: string;
|
text: string;
|
||||||
valid: boolean;
|
valid: boolean;
|
||||||
invalid: boolean;
|
invalid: boolean;
|
||||||
}
|
control: HTMLElement;
|
||||||
interface Field {
|
|
||||||
el: HTMLElement;
|
|
||||||
read: () => ParamState;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function stringValue(value: string): string {
|
function readParameter(parameter: Parameter, control: HTMLElement): ParameterState {
|
||||||
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 wrap = el("span", "cm-field");
|
const input = control.querySelector("input") as HTMLInputElement;
|
||||||
const checkbox = el("input", "cm-input cm-check") as HTMLInputElement;
|
|
||||||
checkbox.type = "checkbox";
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
applyMode();
|
|
||||||
wrap.appendChild(toggle.el);
|
|
||||||
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: {
|
|
||||||
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) return { text: "<数字>", valid: false, invalid: true };
|
const valid = value === "true" || value === "false" || /^b[0-9]$/.test(value);
|
||||||
if (mode.get() === "expression") return { text: value, valid: true, invalid: false };
|
return { text: value || "<布尔>", valid, invalid: value.length > 0 && !valid, control };
|
||||||
const number = Number(value);
|
|
||||||
let valid = isFinite(number);
|
|
||||||
const range = parameter.numericRange;
|
|
||||||
if (valid && range) {
|
|
||||||
if (range.isInteger && !Number.isInteger(number)) 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 };
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
case ParameterType.Enum: {
|
case ParameterType.Enum: {
|
||||||
const wrap = el("span", "cm-field cm-field-string");
|
const input = control.querySelector("input") as HTMLInputElement;
|
||||||
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 value = input.value.trim();
|
||||||
const options = resolveEnumOptions(parameter);
|
const options = resolveEnumOptions(parameter);
|
||||||
const valid = options.length > 0
|
const valid = options.length > 0
|
||||||
? options.some((option) => option.toLowerCase() === value.toLowerCase())
|
? options.some((option) => option.toLowerCase() === value.toLowerCase())
|
||||||
: value.length > 0;
|
: value.length > 0;
|
||||||
return { text: stringValue(value), valid, invalid: !valid };
|
return { text: value || "<选项>", valid, invalid: value.length > 0 && !valid, control };
|
||||||
},
|
}
|
||||||
};
|
case ParameterType.Number: {
|
||||||
|
const input = control as HTMLInputElement;
|
||||||
|
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);
|
||||||
|
let valid = isFinite(number);
|
||||||
|
const range = parameter.numericRange;
|
||||||
|
if (valid && range) {
|
||||||
|
if (range.isInteger && !Number.isInteger(number)) valid = false;
|
||||||
|
if (number < range.min || number > range.max) valid = false;
|
||||||
|
}
|
||||||
|
return { text: value, valid, invalid: !valid, control };
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
const wrap = el("span", "cm-field cm-field-string");
|
const input = control as HTMLInputElement;
|
||||||
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: stringValue(value), valid: value.length > 0, invalid: value.length === 0 };
|
return { text: value ? `str:${value}` : "<文本>", valid: value.length > 0, invalid: false, control };
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Skeleton {
|
function stripStringPrefix(text: string): string {
|
||||||
prefix: string;
|
return text.startsWith("str:") ? text.slice(4) : text;
|
||||||
suffix: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function methodSkeleton(method: Method, hasParams: boolean): Skeleton {
|
function formatMethod(method: Method, states: ParameterState[]): string {
|
||||||
|
const values = states.map((state) => state.text);
|
||||||
|
const args = values.join(", ");
|
||||||
switch (method.type) {
|
switch (method.type) {
|
||||||
case MethodType.Comment: return { prefix: `()=>${method.name}(`, suffix: ")" };
|
case MethodType.Comment:
|
||||||
case MethodType.Ink: return { prefix: `DOTHIS{${method.name}(`, suffix: ")}" };
|
return `()=>${method.name}(${args})`;
|
||||||
case MethodType.Variable: return { prefix: `${method.name} = `, suffix: "" };
|
case MethodType.Ink:
|
||||||
case MethodType.Mod: return { prefix: `"mods": [..., "`, suffix: `"]` };
|
return `DOTHIS{${method.name}(${args})}`;
|
||||||
case MethodType.CustomClass: return { prefix: `"customClass": "`, suffix: `"` };
|
case MethodType.Variable:
|
||||||
|
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 hasParams ? { prefix: `${method.name}(`, suffix: ")" } : { prefix: method.name, suffix: "" };
|
return values.length ? `${method.name}(${args})` : method.name;
|
||||||
case MethodType.Surgery: return { prefix: method.name, suffix: "" };
|
case MethodType.Surgery:
|
||||||
|
return method.name;
|
||||||
default:
|
default:
|
||||||
return hasParams ? { prefix: `${method.name}(`, suffix: ")" } : { prefix: method.name, suffix: "" };
|
return values.length ? `${method.name}(${args})` : method.name;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,7 +456,7 @@
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// fall through
|
// fallthrough to execCommand
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const area = document.createElement("textarea");
|
const area = document.createElement("textarea");
|
||||||
@@ -509,26 +473,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------
|
|
||||||
// 渲染
|
|
||||||
// ---------------------------------------------------------------------
|
|
||||||
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";
|
||||||
|
|
||||||
let currentMethod: Method | null = null;
|
// 优先使用 custommethod.md 里写好的骨架;缺失时再动态生成
|
||||||
|
|
||||||
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 shortEl = container.querySelector<HTMLElement>(".cm-short");
|
let descEl = container.querySelector<HTMLElement>(".cm-desc");
|
||||||
let call = container.querySelector<HTMLElement>(".cm-call");
|
let row = container.querySelector<HTMLElement>(".cm-row");
|
||||||
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 || !shortEl || !call || !preview || !desc) {
|
if (!query || !suggest || !selected || !nameEl || !descEl || !row || !preview) {
|
||||||
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;
|
||||||
@@ -542,68 +500,34 @@
|
|||||||
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");
|
||||||
shortEl = el("span", "cm-short");
|
descEl = el("span", "cm-desc");
|
||||||
head.appendChild(nameEl);
|
head.appendChild(nameEl);
|
||||||
head.appendChild(shortEl);
|
head.appendChild(descEl);
|
||||||
call = el("div", "cm-call");
|
row = el("div", "cm-row");
|
||||||
preview = el("code", "cm-preview");
|
preview = el("code", "cm-preview");
|
||||||
desc = el("div", "cm-desc");
|
|
||||||
selected.appendChild(head);
|
selected.appendChild(head);
|
||||||
selected.appendChild(call);
|
selected.appendChild(row);
|
||||||
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 shortNode = shortEl;
|
const descNode = descEl;
|
||||||
const callEl = call;
|
const rowEl = row;
|
||||||
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);
|
||||||
});
|
});
|
||||||
@@ -614,69 +538,56 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
shortNode.textContent = method.shortDescription || "";
|
descNode.textContent = method.description;
|
||||||
descNode.textContent = method.description || "";
|
rowEl.textContent = "";
|
||||||
callEl.textContent = "";
|
|
||||||
previewEl.textContent = "";
|
previewEl.textContent = "";
|
||||||
copyBtn.hidden = true;
|
copyBtn.hidden = true;
|
||||||
|
|
||||||
const parameters = parametersOf(method);
|
const parameters = parametersOf(method);
|
||||||
const fields: Field[] = [];
|
const controls: { parameter: Parameter; control: HTMLElement }[] = [];
|
||||||
|
|
||||||
function updatePreview(): void {
|
function updatePreview(): void {
|
||||||
const states = fields.map((field) => field.read());
|
const states = controls.map((item) => readParameter(item.parameter, item.control));
|
||||||
fields.forEach((field, i) => field.el.classList.toggle("cm-invalid", states[i].invalid));
|
states.forEach((state) => state.control.classList.toggle("cm-invalid", state.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 = "该方法没有参数";
|
||||||
callEl.appendChild(empty);
|
rowEl.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();
|
||||||
for (const method of searchMethods(queryEl.value)) {
|
const matches = 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.shortDescription || method.description;
|
descSpan.textContent = method.description;
|
||||||
item.appendChild(nameSpan);
|
item.appendChild(nameSpan);
|
||||||
item.appendChild(descSpan);
|
item.appendChild(descSpan);
|
||||||
item.addEventListener("mousedown", (event) => {
|
item.addEventListener("mousedown", (event) => {
|
||||||
@@ -702,7 +613,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initAll(): void {
|
function initAll(): void {
|
||||||
document.querySelectorAll<HTMLElement>(".custommethods-search").forEach((container) => renderContainer(container));
|
const containers = document.querySelectorAll<HTMLElement>(".custommethods-search");
|
||||||
|
containers.forEach((container) => renderContainer(container));
|
||||||
}
|
}
|
||||||
|
|
||||||
function register(): void {
|
function register(): void {
|
||||||
@@ -722,9 +634,11 @@
|
|||||||
search: searchMethods,
|
search: searchMethods,
|
||||||
editDistance,
|
editDistance,
|
||||||
renderAll: initAll,
|
renderAll: initAll,
|
||||||
strategies,
|
|
||||||
getMethods: () => methods,
|
getMethods: () => methods,
|
||||||
setMethods: (list: Method[]) => { methods = list; initAll(); },
|
setMethods: (list: Method[]) => {
|
||||||
|
methods = list;
|
||||||
|
initAll();
|
||||||
|
},
|
||||||
Parameter,
|
Parameter,
|
||||||
ParameterType,
|
ParameterType,
|
||||||
Method,
|
Method,
|
||||||
|
|||||||
@@ -485,96 +485,4 @@ 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user