Skip to content

Commit

Permalink
feat: 优化输入联想的交互体验
Browse files Browse the repository at this point in the history
  • Loading branch information
sunsonliu committed Jul 6, 2023
1 parent 085b5ea commit 2dcb231
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 84 deletions.
4 changes: 0 additions & 4 deletions src/Cherry.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,6 @@ const defaultConfig = {
// 'hookName': {
//
// }
// 启用输入联想功能
suggester: {
suggester: [{}],
},
autoLink: {
/** 是否开启短链接 */
enableShortLink: true,
Expand Down
2 changes: 1 addition & 1 deletion src/Engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export default class Engine {
});
this.initMath(markdownParams);
this.$configInit(markdownParams);
this.hookCenter = new HookCenter(hooksConfig, markdownParams);
this.hookCenter = new HookCenter(hooksConfig, markdownParams, cherry);
this.hooks = this.hookCenter.getHookList();
this.md5Cache = {};
this.md5StrMap = {};
Expand Down
6 changes: 5 additions & 1 deletion src/core/HookCenter.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ export default class HookCenter {
* @param {(typeof SyntaxBase)[]} hooksConfig
* @param {Partial<CherryOptions>} editorConfig
*/
constructor(hooksConfig, editorConfig) {
constructor(hooksConfig, editorConfig, cherry) {
this.$locale = cherry.locale;
/**
* @property
* @type {Record<import('./SyntaxBase').HookType, SyntaxBase[]>} hookList hook 名称 -> hook 类型的映射
Expand Down Expand Up @@ -233,6 +234,9 @@ export default class HookCenter {
// TODO: 需要考虑自定义 hook 配置的传入方式
const config = syntax?.[hookName] || {};
instance = new HookClass({ externals, config, globalConfig: engine.global });
instance.afterInit(() => {
instance.setLocale(this.$locale);
});
}
// TODO: 待校验是否需要跳过禁用的自定义 hook
// Skip Disabled Internal Hooks
Expand Down
11 changes: 11 additions & 0 deletions src/core/SyntaxBase.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export default class SyntaxBase {
* @type {import('../Engine').default}
*/
$engine;
$locale;

/**
* @constructor
Expand All @@ -69,6 +70,16 @@ export default class SyntaxBase {
return /** @type {typeof SyntaxBase} */ (this.constructor).HOOK_NAME;
}

afterInit(callback) {
if (typeof callback === 'function') {
callback();
}
}

setLocale(locale) {
this.$locale = locale;
}

/**
* 生命周期函数
* @param {string} str 待处理的markdown文本
Expand Down
193 changes: 115 additions & 78 deletions src/core/hooks/Suggester.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { Pass } from 'codemirror/src/util/misc';
import { isLookbehindSupported } from '@/utils/regexp';
import { replaceLookbehind } from '@/utils/lookbehind-replace';
import { isBrowser } from '@/utils/env';
import locales from '@/locales/index';
import Cherry from '@/Cherry';

/**
Expand All @@ -33,10 +34,10 @@ import Cherry from '@/Cherry';

/**
* @typedef { Object } SuggestListItemObject 推荐列表项对象
* @property { string } type 类型
* @property { string } icon 图标
* @property { string } text 文本
* @property { string= } sub 推荐列表项子项(常用于列表项有子项的情况,例如:header)
* @property { string } label 候选列表回显的内容
* @property { string } value 点击候选项的时候回填的值
* @property { string } keyword 关键词,通过关键词控制候选项的显隐
* @typedef { SuggestListItemObject | string } SuggestListItem 推荐列表项
* @typedef { Array<SuggestListItem> } SuggestList 推荐列表
*/
Expand Down Expand Up @@ -75,66 +76,110 @@ export default class Suggester extends SyntaxBase {

super({ needCache: true });

this.initConfig(config);
this.config = config;
this.RULE = this.rule();
}

afterInit(callback) {
if (typeof callback === 'function') {
callback();
}
this.initConfig(this.config);
}

/**
* 获取系统默认的候选项列表
* TODO:后面考虑增加层级机制,比如“公式”是一级,“集合、逻辑运算、方程式”是公式的二级候选值
*/
getSystemSuggestList() {
const locales = this.$locale;
return [
{
icon: 'h1',
label: locales['H1 Heading'],
keyword: 'head1',
value: '# ',
},
{
icon: 'h2',
label: locales['H2 Heading'],
keyword: 'head2',
value: '## ',
},
{
icon: 'h3',
label: locales['H3 Heading'],
keyword: 'head3',
value: '### ',
},
{
icon: 'table',
label: locales.table,
keyword: 'table',
value: '| Header | Header | Header |\n| --- | --- | --- |\n| Content | Content | Content |\n',
},
{
icon: 'code',
label: locales.code,
keyword: 'code',
value: '```\n\n```\n',
},
{
icon: 'link',
label: locales.link,
keyword: 'link',
value: `[title](https://url)`,
},
{
icon: 'checklist',
label: locales.checklist,
keyword: 'checklist',
value: `- [ ] item\n- [x] item`,
},
{
icon: 'tips',
label: locales.panel,
keyword: 'panel tips info warning danger success',
value: `::: primary title\ncontent\n:::\n`,
},
{
icon: 'insertFlow',
label: locales.detail,
keyword: 'detail',
value: `+++ 点击展开更多\n内容\n++- 默认展开\n内容\n++ 默认收起\n内容\n+++\n`,
},
];
}

/**
* 初始化配置
* @param {SuggesterConfig} config
*/
initConfig(config) {
const { suggester } = config;
let { suggester } = config;

this.suggester = {};
if (!suggester) {
return;
suggester = [];
}
const systemSuggestList = this.getSystemSuggestList();
// 默认的唤醒关键字
suggester.unshift({
keyword: '/',
suggestList(word, callback) {
callback([
{
type: 'header',
icon: 'h1',
sub: 'h1',
text: 'H1 一级标题',
},
{
type: 'header',
icon: 'h2',
sub: 'h2',
text: 'H2 二级标题',
},
{
type: 'header',
icon: 'h3',
sub: 'h3',
text: 'H3 三级标题',
},
{
type: 'quickTable',
icon: 'table',
text: '快捷表格',
},
{
type: 'codeBlock',
icon: 'code',
text: '代码块',
},
{
type: 'graph',
sub: 'insertFlow',
icon: 'insertFlow',
text: '流程图',
},
{
type: 'formula',
icon: 'insertFormula',
text: '公式',
},
]);
const $word = word.replace(/^\//, '');
// 加个空格就直接退出联想
if (/^\s$/.test($word)) {
callback(false);
return;
}
const keyword = $word.replace(/\s+/g, '').split('').join('.*?');
const test = new RegExp(`^.*?${keyword}.*?$`, 'i');
const suggestList = systemSuggestList.filter((item) => {
// TODO: 首次联想的时候会把所有的候选项列出来,后续可以增加一些机制改成默认拉取一部分候选项
return !$word || test.test(item.keyword);
});
callback(suggestList);
},
});
suggester.forEach((configItem) => {
Expand Down Expand Up @@ -362,7 +407,7 @@ class SuggesterPanel {
let defaultValue = suggestList
.map((suggest, idx) => {
if (typeof suggest === 'object' && suggest !== null) {
let renderContent = suggest.text;
let renderContent = suggest.label;
if (suggest?.icon) {
renderContent = `<i class="ch-icon ch-icon-${suggest.icon}"></i>${renderContent}`;
}
Expand Down Expand Up @@ -455,7 +500,6 @@ class SuggesterPanel {
this.cursorFrom = from;
this.keyword = keyword;
this.searchCache = true;
this.searchKeyCache = [keyword];
this.relocatePanel(codemirror);
}

Expand Down Expand Up @@ -487,8 +531,12 @@ class SuggesterPanel {
const { cursorFrom, cursorTo } = this; // 缓存光标位置
if (this.optionList[idx]) {
let result = '';
if (typeof this.optionList[idx] === 'object' && this.optionList[idx] !== null) {
result = this.matchQuickTool(this.optionList[idx], this.editor.$cherry);
if (
typeof this.optionList[idx] === 'object' &&
this.optionList[idx] !== null &&
typeof this.optionList[idx].value === 'string'
) {
result = this.optionList[idx].value;
}
if (typeof this.optionList[idx] === 'string') {
result = ` ${this.keyword}${this.optionList[idx]} `;
Expand All @@ -500,29 +548,6 @@ class SuggesterPanel {
}
}

/**
* 匹配快捷工具
* @param {SuggestListItemObject} suggester
* @param {import('../../Cherry').default} cherry
* @returns {string}
*/
matchQuickTool(suggester, cherry) {
if (typeof suggester !== 'object' || suggester === null) return '';
if (cherry instanceof Cherry === false) return '';
const { type = '', sub = '' } = suggester;
const quickToolMap = {
header: (sub) => cherry.toolbar.menus.hooks.header.onClick('/', sub),
quickTable: () => cherry.floatMenu.menus.hooks.quickTable.onClick(''),
codeBlock: () => cherry.floatMenu.menus.hooks.code.onClick(''),
graph: (sub) => cherry.toolbar.menus.hooks.graph.subMenuConfig.find((menu) => menu.name === sub)?.onclick(),
formula: () => cherry.toolbar.menus.hooks.formula.onClick(''),
};
if (typeof quickToolMap[type] === 'function') {
return quickToolMap[type](sub) ?? '';
}
return '';
}

/**
* 寻找当前选中项的索引
* @returns {number}
Expand All @@ -547,9 +572,11 @@ class SuggesterPanel {
const { text, from, to, origin } = evt;
const changeValue = text.length === 1 ? text[0] : '';

if (this.suggesterConfig[changeValue]) {
// 首次输入命中关键词的时候开启联想
if (!this.enableRelate() && this.suggesterConfig[changeValue]) {
this.startRelate(codemirror, changeValue, from);
} else if (this.enableRelate() && (changeValue || origin === '+delete')) {
}
if (this.enableRelate() && (changeValue || origin === '+delete')) {
this.cursorTo = to;
if (changeValue) {
this.searchKeyCache.push(changeValue);
Expand All @@ -564,10 +591,13 @@ class SuggesterPanel {
if (typeof this.suggesterConfig[this.keyword]?.suggestList === 'function') {
// 请求api 返回结果拼凑
this.suggesterConfig[this.keyword].suggestList(this.searchKeyCache.join(''), (res) => {
if (!res || !res.length) {
// 如果返回了false,则强制退出联想
if (res === false) {
this.stopRelate();
return;
}
this.optionList = res;
// 回显命中的结果
this.optionList = !res || !res.length ? [] : res;
this.updatePanel(this.optionList);
});
}
Expand Down Expand Up @@ -619,6 +649,13 @@ class SuggesterPanel {
setTimeout(() => {
this.stopRelate();
}, 0);
} else if (keyCode === 27) {
// 按下esc的时候退出联想
evt.stopPropagation();
codemirror.focus();
setTimeout(() => {
this.stopRelate();
}, 0);
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/locales/en_US.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,7 @@ export default {
exportScreenshot: 'Screenshot',
exportMarkdownFile: 'Export Markdown File',
exportHTMLFile: 'Export preview HTML File',
'H1 Heading': 'H1 Heading',
'H2 Heading': 'H1 Heading',
'H3 Heading': 'H1 Heading',
};
3 changes: 3 additions & 0 deletions src/locales/zh_CN.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,7 @@ export default {
theme: '主题', // 导出长图
panel: '面板', // 导出长图
detail: '手风琴', // 手风琴
'H1 Heading': 'H1 一级标题',
'H2 Heading': 'H2 二级标题',
'H3 Heading': 'H3 三级标题',
};

0 comments on commit 2dcb231

Please sign in to comment.