[建议]尽可能的利用css去计算?
-
1.用hidden=!0 替代 style.display=none更有性价比。
-
2.编辑区和预览区放在一个容器里面,方面当竖屏可以竖排而不是只能横排溢出
-
3.很明显给主容器加
height:100%没什么用,不如增加几层DIV,flex垂直布局,让编辑预览区自动适应高度,这样无需要JS去设置min-height
CSS变量化建议
一些浮动定位数据用var(--cherry-toolbar-xxx) 添加一个唯一的随机id标记主容器,类似这样
function creatSheet(){
var style = document.querySelector('#cherry-css-var');
if(!style)){
style = document.head.appendChild(document.createElement('style'));
style.setAttribute('id','cherry-css-var');
}
return style.sheet;
}
var css_var = {};
var css_pos;
var css_id = '#cherry-'+Math.random().toString().slice(2);
function updateVar(obj,prefix){
let sheet = creatSheet();
prefix = prefix?'-'+prefix+'-':'-';
Object.entries(obj).forEach(entry=>{
css_var['--cherry'+prefix+entry[0]] = entry[1]?entry[1]:'';
});
Object.assign(css_var,obj);
if(css_pos===undefined){
css_pos = sheet.rules.length;
}
if(sheet.rules[css_pos])sheet.deleteRule(css_pos);
sheet.insertRule(css_id+'{'+ Object.entries(css_var).map(v=>v[0]+':'+v[1]).join(';')+'}',css_pos);
}
updateVar({top:'0px'},'toolbar');
这些理论上dom几乎无修改,而且css calc min max运算有时候比js好用,特别自适应方面。
br9 不如在css中增加 style="--br-line:9" 这样CSS中可以直接根据值height:calc(var(--br-line) * .5rem)
可以提供最小可复现的代码以便排查问题。
br9 不如在css中增加 style="--br-line:9" 这样CSS中可以直接根据值
height:calc(var(--br-line) * .5rem)
9 不参与高度计算。
9 不参与高度计算。
我知道,但是如果我需要计算值,那么可能需要span[data-sign="br9"]{}去单独计算,或者通过JS
- GFW是会把两个回车当成BR,或者字符后面加两个空格表示行尾,所以忽略回车的换行并不妥.
而当使用 --br-line这样时,可以通过 span[style*="--br-lines"] 就可以一次性设置.JS也很方便查询,获取值也简单style.getPropertyValue('--br-lines')
不是一堆值标记不好,而是利用CSS更有性价比!
比起 elment.dataset.xxx
function getValue(elm,name){
return elm.style.getPropertyValue(name)
}
难道不香吗?而且还不需要判断值是否存在,不存在永远返回''
获取所有值 Object.fromEntries(toItem(elm.style))
function tryfn(o, fn, a) {
if (o) {
if (fn && typeof value =='string') fn = o[fn];
return fn instanceof Function ?fn.bind(o, ...a)():undefined;
}
}
function toItem(o) {
//把 style attr 转换为 json 数组对象
var a = [];
for (let i = 0; i < o.length; i++) {
var value = o.item(i);
if (typeof value =='string') {
var keyValue = tryfn(o, o.getPropertyValue, value);
if (keyValue) a.push([value,keyValue]);
else a.push(value);
} else if (value) {
if (value.nodeType === 2) {
a.push([value.name, value.value]);
} else {
a.push(value);
}
}
}
return a;
}
@sunsonliu
!!#000000 !!!#f9cb9c 又更新啦呀,666 !!!!!
!!#000000 !!!#cc0000 !32 又更新啦呀,666!!!!!!
字体背景色,当我先把字体改成更大时,再增加背景色.背景色就有问题.
以前没有文字阴影,但是现在有了,我认为背景色改成text-shadow效果更好,更能体现"文字背景色"或者更有强调效果而不影响美观.
@sunsonliu
可以提供最小可复现的代码以便排查问题。
src里面没有源代码。不过通过搜索,空白时,是有Js指定min-height:57px
关键字
display.sizer.style.minHeight =
.docHeight
display.heightForcer.style.top
9 不参与高度计算。
我知道,但是如果我需要计算值,那么可能需要
span[data-sign="br9"]{}去单独计算,或者通过JS
- GFW是会把两个回车当成BR,或者字符后面加两个空格表示行尾,所以忽略回车的换行并不妥.
而当使用
--br-line这样时,可以通过span[style*="--br-lines"]就可以一次性设置.JS也很方便查询,获取值也简单style.getPropertyValue('--br-lines')不是一堆值标记不好,而是利用CSS更有性价比! 比起
elment.dataset.xxxfunction getValue(elm,name){ return elm.style.getPropertyValue(name) }难道不香吗?而且还不需要判断值是否存在,不存在永远返回
''获取所有值
Object.fromEntries(toItem(elm.style))function tryfn(o, fn, a) { if (o) { if (fn && typeof value =='string') fn = o[fn]; return fn instanceof Function ?fn.bind(o, ...a)():undefined; } } function toItem(o) { //把 style attr 转换为 json 数组对象 var a = []; for (let i = 0; i < o.length; i++) { var value = o.item(i); if (typeof value =='string') { var keyValue = tryfn(o, o.getPropertyValue, value); if (keyValue) a.push([value,keyValue]); else a.push(value); } else if (value) { if (value.nodeType === 2) { a.push([value.name, value.value]); } else { a.push(value); } } } return a; }@sunsonliu
项目当前还没有类似的需求场景,其实 data-sign 跟 css 或样式没有任何联系哈。如果遇到了实际问题可以提供具体的场景以及最小可复现的源码,我们来评估具体的场景。比如,上面提到的布局问题。
另外,关于css 变量化这一点是在规划内的,但目前仅针对主题色有规划变量化。
可以提供最小可复现的代码以便排查问题。
src里面没有源代码。不过通过搜索,空白时,是有Js指定min-height:57px
关键字 display.sizer.style.minHeight = .docHeight display.heightForcer.style.top
那就是 codemirror 的源码了
项目当前还没有类似的需求场景,其实 data-sign 跟 css 或样式没有任何联系哈。如果遇到了实际问题可以提供具体的场景以及最小可复现的源码,我们来评估具体的场景。比如,上面提到的布局问题。
另外,关于
css 变量化这一点是在规划内的,但目前仅针对主题色有规划变量化。
sign是标记嘛,我知道没联系。只不过是举例罢了。
例如data-lines额外添加css属性“--lines:”,当然比较推荐的是大于2(n-1)添加--br-line:这样连class="hight-line"都可以省略,进一步减少代码的css类使用。
另外导出Html应该过滤掉data-sign data-lines不必要的代码,这样对数据库储存剩下那么一点小空间。
可以提供最小可复现的代码以便排查问题。
src里面没有源代码。不过通过搜索,空白时,是有Js指定min-height:57px
关键字 display.sizer.style.minHeight = .docHeight display.heightForcer.style.top
那就是 codemirror 的源码了
一半一半吧,因为他是初始化时检查高度,而你主容器是height:100%。所以无效高度。解决方法就是flex竖排,还有里面居然用了position fixed,这样会挡住其他下拉框。除了菜单用绝对定位,其他布局适应用flex比起固定定位更好一点吧?
可以提供最小可复现的代码以便排查问题。
src里面没有源代码。不过通过搜索,空白时,是有Js指定min-height:57px
关键字 display.sizer.style.minHeight = .docHeight display.heightForcer.style.top
那就是 codemirror 的源码了
一半一半吧,因为他是初始化时检查高度,而你主容器是height:100%。所以无效高度。解决方法就是flex竖排,还有里面居然用了position fixed,这样会挡住其他下拉框。除了菜单用绝对定位,其他布局适应用flex比起固定定位更好一点吧?
position:fixed 只有全屏模式才会使用到的,其他应用到 fixed 定位的都是一些浮窗元素。还是建议提供一下最小可复现的代码。
!!#000000 !!!#f9cb9c 又更新啦呀,666 !!!!! !!#000000 !!!#cc0000 !32 又更新啦呀,666!!!!!!字体背景色,当我先把字体改成更大时,再增加背景色.背景色就有问题.
以前没有文字阴影,但是现在有了,我认为背景色改成
text-shadow效果更好,更能体现"文字背景色"或者更有强调效果而不影响美观.@sunsonliu
收到,不过这里对“文字背景色”的理解可能会有出入,一般用户(尤其是没有css背景的用户)会把这个功能理解成“突出显示”(如下图),并且用户反馈也的确是需要一个“能突出某些内容”的功能,这里为了避免产生歧义,我们跟其他编辑器对齐,把“背景颜色”改名成“突出显示”吧
然后 背景色和文字大小冲突的问题,我们也跟进下,感谢反馈~
然后 背景色和文字大小冲突的问题,我们也跟进下,感谢反馈~ 我觉得背景色完全没必要,因为现在不是WEB2.0那个
<font>时代,LCD屏幕没有CRT那种色彩逼真还原程度,看起来特别难看.因为替换成阴影色就顺眼很多.
继续说说CSS变量化可行性(非主题配色方向) 简化的事件处理. @sunsonliu @lyngai
利用过渡事件
//给所有菜单(或者菜单内容容器)增加 background-color:var(--cherry-menu-xx); 以及共有CSS transition:background-color .3s ease-in;
//有一条专用toolbar的菜单 css规则(#C),菜单0-9, 假设点击菜单5那么触发向#C写入--cherry-menu-5:#000 (也可以是"var(--cherry-menu-hover-color) 作为公共值");
//点击菜单5,因为"--cherry-menu-5"不存在,进行添加,菜单5触发过渡事件 触发 transition 事件 transitionend menuDIV.hidden == !0
//再次点击菜单5,因为"--cherry-menu-5" 是存在,所以进行删除操作, 再次触发transitionend, 如果不存在"--cherry-menu-5" 设置 menuDIV.hidden != !0
//菜单容器 可以增加一个pointerout 或者干脆不添加.或者里面的任何特定操作触发 删除"--cherry-menu-5" 实现选择后隐藏.
//这样可以免除mousedown等事件滥用,transition独立与元素,不会冒泡.
//菜单5涉及两个交互事件 pointerdown/up(主要点击事件),一个隐藏transition,即可完成事件,菜单也无需像传统方式遍历一遍关闭和开启
var rgb1 = [0,0,0];
var rgb2 = [255,255,255];
var text = "CSS的过渡事件例子";
var html = "";
var color = [];
var css_text = [];
var sheet = document.head.appendChild(document.createElement('style')).sheet;
var id = 't-'+Math.random().toString().slice(2);
for(var i=0;i<text.length;i++){
html += '<span>'+text.charAt(i)+'</span>';
var num = i==text.length-1?1:i/text.length;
color.push('#'+rgb2.map((v,k)=>Math.abs(Math.floor(rgb1[k]+(v-rgb1[k])*num)).toString(16).padStart(2,'0')).join(''));
sheet.insertRule('#'+id+' span:nth-child('+(i+1)+'){color:var(--c-'+i+','+color[color.length-1]+');}',sheet.cssRules.length);
};
sheet.insertRule('#'+id+' span{transition:all .15s ease-in;}',sheet.cssRules.length);
var div = document.body.appendChild(document.createElement('div'));
div.innerHTML = html;
var pos = undefined;
var stop = false;
div.setAttribute('id',id);
function setColor(){
if(stop) return;
if(pos!=undefined&&sheet.cssRules[pos]){
sheet.deleteRule(pos);
let last = color.pop();
color.unshift(last);
}else{
pos = sheet.cssRules.length;
}
sheet.insertRule('#'+id+'{'+color.map((v,k)=>'--c-'+k+':'+v).join(';')+'}',pos);
}
setColor();
div.firstChild.addEventListener('transitionend',e=>{
console.log(e);
window.requestAnimationFrame(e=>setColor());
});
window.requestAnimationFrame(()=>{
div.firstChild.dispatchEvent(new CustomEvent('transitionend'));
});
div.addEventListener('pointermove',e=>{
stop = true;
});
div.addEventListener('pointerout',e=>{
stop = false;
setColor();
});
继续说说CSS变量化可行性(非主题配色方向) 简化的事件处理. @sunsonliu @lyngai
事实上,这种做法并没有真正做到事件处理的简化,这里有点为了使用技巧而使用技巧的意思在了,反而增加了用户问题排查成本与开发成本。
以 transition 系列事件为例,目前项目中并没有遇到需要等待 transition 结束才能做的事情。一个菜单按钮点击的事件流也并没有复杂到需要操纵 css 变量才能完成,引入 eventbus 也能达到想要的效果。项目秉承的一个原则是 css 尽量只处理样式和布局相关的逻辑,不过多涉及通过 js 正常的事件流就能完成的逻辑。
综上,以上的建议在目前的项目场景看来意义不大,建议您还是提出基于实际遇到问题的场景,我们才好去评估是否落地这些 tricks。
事实上,这种做法并没有真正做到事件处理的简化,这里有点为了使用技巧而使用技巧的意思在了,反而增加了用户问题排查成本与开发成本。
以
transition系列事件为例,目前项目中并没有遇到需要等待transition结束才能做的事情。一个菜单按钮点击的事件流也并没有复杂到需要操纵 css 变量才能完成,引入 eventbus 也能达到想要的效果。项目秉承的一个原则是 css 尽量只处理样式和布局相关的逻辑,不过多涉及通过 js 正常的事件流就能完成的逻辑。综上,以上的建议在目前的项目场景看来意义不大,建议您还是提出基于实际遇到问题的场景,我们才好去评估是否落地这些 tricks。
也是的,最初我是想利用CSS3的特色平滑替代setTimeOut,菜单出现时更有动态感觉.不过想想也是花里胡巧的,没啥实际意义.
用hidden代替display:none 应该可以进行吧? 也好判断,不用重新显示的时候赋值 block
@lyngai
-
采用外挂字体形式,不在css中使用编写特定图标css规则,,工具菜单直接使用 icon:'\exxx' 或者
<svg>或者url(x.svg)let file= await getStore('libjs').ajax('font/ch-icon-v1.0.woff'); await u.addFont('ch-icon',file); let className = addIcon(name,data,type='before',iconFont) -
主题配置增加额外参数,使得可以外挂主题样式,或者设置css变量
-
插件可以缓存CDN,例如流程图,满足又不想用大包,又不想挂自己流量,以及加载速度.
await addJS('https://unpkg.com/[email protected]/dist/mermaid.min.js',false,false) -
如果没设置上传服务器接口,本地环境编辑时,先把图片转换webp(这个格式基本上都支持,包括QQ浏览器),然后储存到indexDB, 生成一个 images['xxx.jpg'] = 'blob://....' 在导出HTML时,把这些地址转化成base64
-
删除默认数据,当空值时读取草稿,可以保存草稿,备份草稿.查看草稿列表
utils.getStore('temp').all('timestamp',null,!0); 获取草稿列表信息 {'草稿名 ': '2023 11/25 xxx utc'}
/**
* 通用工具方法
* @important 减少代码体积
* @important 减少繁琐配置
* @important 加强配置信息灵活性
* @important 减少对核心CSS自定义花销
* 辅助类
* CSS 管理器
* cssRule 参数缺省,默认值utilsTool.sheet;
* @method insertRule(ruletxt,index,cssRule) 插入一条规则,index可选,是插入位置. 返回插入规则位置.
* @method updateRule(ruletxt,index,cssRule) 删除并更新指定位置的
* @method deleteRule(index,cssRule) 删除指定位置规则
* @method putCssText(cssValue,index,cssRule) 覆盖规则变量值,css规则不变化,但是内容替换
* @method addCssText(cssValue,index,cssRule) 增加规则变量值,css规则不变化,内容重复就覆盖否则添加
* @method emptyCssText(index,cssRule) 指定位置的CSS规则 条目重置
* @method matchRule(reg,index,cssRule) 正则检查指定位置的CSS规则的选择符
* @method getRule(index,cssRule) 指定位置的CSS规则值 {key:value} 特别注意,谷歌内核会把颜色代码会自动转化为rgb()
* @method AllRules(cssRule) CSS规则值 [ {'.box':{key:value}},...]
* @method ruleSize(index,cssRule) 指定位置的CSS规则 条目数量
* @method ruleItem(index,cssRule) 获取指定位置的 返回 CSSStyleRule
* CSSStyleRule:{
* style:完全等价于elment.style,elment.style.cssText可写 不影响子规则
* cssText:只读 是整个CSS规则标目,包括子规则, 如".body {\n color:red;\n .b{ color:blue; }\n}" 类似scss;
* parentRule:只读 此规则的上级规则,顶级条目,值为null
* parentStyleSheet:只读 此规则的源 如果<link>:xxx.css <style>:text/css
* }
* @important 用途
* 1.工具栏二级菜单唯一性,因此定义一个index<insertRule>,只需要写入该规则需要显示时css位置信息<updateRule>,隐藏时只需要emptyCssText.影响调试应该不会,因为点击对象时控制台可看CSS信息
* 2.主题管理,应该实现类似
* [{
* className:'defalut',
* label: '默认',
* cssText:{
* '--xxx':'red' //变量替换 .cherry[data-name]{--xxx:red};
* 'Rule':[
* {'.xx .xx{color:red;}'} //规则覆盖 .cherry[data-name] .xx .xx{color:red;}
* ]
* },
* link:'xxx.css',可定义主题独立CSS文件
* ajax:'url' 通过AJAX请求动态更新服务端的主题配置,大幅降低配置的内容
* utils.ajax({
* url,
* hook:'theme-mystle'
* //headers:{'cherry-theme':'xxx'}
* //type:'json'
* });
* 或者创建类似 utils.getStore('theme').ajax 的缓存方案
* }]
*
* 字体管理
* @method addFont(name,url,options) 写入一个字体
* @var fontList 已载入字体列表
* @var iconFont 默认图标字体
* @var fontPrefix 图标字体样式前缀
* @method addIcon(name,data,type='before',iconFont) 添加一个字体图标 返回图标className
* @method getIcon(name) 或者字体类图标className
* @important 用途
* 配置文件增加{fonts:{'ch-icon':'fonts/ch-icon.*'}};,实现字体动态管理.
* 编辑模式 加载ch-icon<await addFont>
* 遍历工具菜单,根据菜单的icon值<addIcon>并且给菜单按钮增加对应className
* 这样无需增加额外的修改核心css花销,直接配置化
* Aajx 交互 上传 回调
* @method ajax(ARG) 默认添加header请求头 ajax-fetch:cherry
* @var url 地址
* @var hook, //插件名
* @var type, //定义返回类型 留空则被认为自动. "json,blob,u8,text,xml,html,js,javascript" javascript会是返回文件对象,js是返回文本
* @var success, //回调函数
* @var progress, //进度函数 进度函数三个参数 progress(固定参数:'request'|'upload','下载或者上传 多少数据','总数据')
* @var headers = {'ajax-fetch':'cherry'}, //设置请求头,替代以前get?name=xxx去实现ajax无刷新AJAX请求
* @var error, //失败函数
* @var paramsDictionary,
* @var post, //可以是表单对象 也可以是POST值 {key:value(文本或者二进制数据)} 若需要输送多个值 post = new FormData();post.append(key,value1);post.append(key,value2);
* @var params, //地址GET附加参数
* @var json, POST发送文件去客户端,PHP服务端需要用$json = file_get_contents('php://input'); 读取.
* @important 外国很常用这种方式非$_POST数组的,自然肯定有他的优点吧
* headers{'ajax-fetch':'cherry'} PHP环境下 $_SEVER['HTTP_AJAX_FETCH'] == 'cherry'
* 菜单插件中使用添加 hook='image' 那么对应服务器请求头就有 $_SEVER['HTTP_CHERRY_HOOK'] == 'image'
* 过去指定地址,一堆get字符,等操作的显式地址,容易参数过多.造成混乱,现在这样隐式的请求头更隐蔽安全(蜘蛛爬虫不会触发),其多样性也可以减少被恶意HTTP抓取.
* 也方便网站可以在任意位置处理请求,无需增加URL地址重复一堆权限验证.
*
* @method addJS(url,iscss,cache) cheche 设置false 或者名称 将会进行缓存,false会用文件名 否则用指定名
* @important 注意地址是否支持跨域请求.国外CDN一般都可以,否则不要设置cache,地址中的@10.6.1 作为版本管理
* @important 若无版本请用cache:false ./js/jq-1.js这种格式,避免缓存导致用户执行的内容不更新!
* @example await addJS('https://unpkg.com/[email protected]/dist/mermaid.min.js',false,false)
* 缓存管理
* @var storeMap 初始化数据库表 {xxx:{},kk:{type:false}} type用于indexDB范围条件查询
* @var storeName 数据库名
* @method getStore(table) 获取一个表
* //如 await getStore('fonts').getItem('ch-icon');
* @method getItem(name) 在表中取得数据<异步>
* @method removeItem(name) 在表中删除键<异步>
* @method setItem(name,data) 在表写入数据
* @example 保持草稿 setItem('xxx.jpg',{contents:Inages,type:'image',timestamp:new Date});
* @method getData(name,version) 兼容格式 如果版本不一致返回null 如果数据不是{contents:data}格式储存那么返回原始数据<异步>
*
* @method setData(name,data,version) 兼容格式 数据以{contents:data,version,timestamp} 储存
* @example 保持草稿 setData('xxx.jpg',Inages,{type:'image',timestamp:new Date});
* @method all(index,range,bool) 全部数据 可选 index对应上面的type, range:IDBKeyRange.only('image');
* @important 注意,如果不是通过兼容风格保持数据 或者设置 表密匙{index:false} ,请不要设置三个参数
* @await utils.getStore('files').all('type',IDBKeyRange.only('image'));
* @await utils.getStore('temp').all(); 获取所有草稿数据(不推荐)
* @await utils.getStore('temp').all('timestamp',null,!0); 获取全部草稿名和保存时间,当用户选择时再用getData获取
* @method keys(index,range) 同上 获取键列表
* @method ajax(url,name,version) 此方法会从URL中读取并且缓存
* @example await utils.getStore('libjs').ajax('https://unpkg.com/[email protected]/dist/mermaid.min.js');
* 转换
* @method toBase64(file,type) 转换为BASE64地址 如(await k.toBase64("测试一下",'text/plain')).slice(23) 得到纯base64,否则是data:text/plain;base64,
* @method toWebp(file,type,quality) 转换为更节省空间的webp quality转换质量,默认1,越少失真越严重类似jpg
*/
// deno-lint-ignore-file no-this-alias
/**
* 辅助类
*/
export default class utils {
constructor(cherry) {
Object.defineProperties(this, {
cherry: {
get() {
return cherry;
}
},
optons: {
get() {
return cherry.options;
}
}
});
}
sheet_id = "#cherry-css-var";
/**
* @returns {CSSStyleSheet}
*/
initSheet() {
/**
* @const {HTMLStyleElement}
*/
const style = document.querySelector(this.sheet_id) || document.head.appendChild(document.createElement('style'));
if (!style.getAttribute('id')) {
style.setAttribute('id', this.sheet_id.slice(1));
}
return style.sheet || document.styleSheets[0];
}
/**
* @type {CSSStyleSheet}
*/
sheet = this.initSheet();
/**
* 插入一条CSS规则
* @param {String} ruletxt
* @param {number|undefined} index
* @param {CSSStyleRule|undefined} cssRule
* @returns {number}
*/
insertRule(ruletxt, index, cssRule) {
if (!cssRule) cssRule = this.sheet;
index = isNaN(index) ? cssRule.cssRules.length : parseInt(index);
return cssRule.insertRule(ruletxt, index);
}
/**
* 插入或者更新 一条CSS规则
* @param {String} ruletxt
* @param {number|undefined} index
* @param {CSSStyleRule|undefined} cssRule
* @returns {number}
*/
updateRule(ruletxt, index, cssRule) {
const nowRule = this.ruleItem(index, cssRule);
if (nowRule) {
nowRule.parentRule.deleteRule(index);
nowRule.parentRule.insertRule(ruletxt, index);
}
}
/**
* 删除指定位置的CSS规则
* @param {number} index
* @param {CSSStyleRule|undefined} cssRule
*/
deleteRule(index, cssRule) {
const nowRule = this.ruleItem(index, cssRule);
if (nowRule) {
nowRule.parentRule.deleteRule(index);
}
}
/**
* 指定位置的CSS规则条目数量
* @param {number} index
* @param {CSSStyleRule|undefined} cssRule
*/
ruleSize(index, cssRule) {
const nowRule = this.ruleItem(index, cssRule);
if (nowRule) {
return nowRule.styleMap.size;
}
return 0;
}
/**
* 查找指定位置规则
* @param {number} index 规则位置
* @param {CSSStyleRule|undefined} cssRule
* @returns {CSSStyleRule|null} 返回CSS规则
*/
ruleItem(index, cssRule) {
if (isNaN(index)) return null;
const nowRule = (cssRule || this.sheet).cssRules;
return nowRule ? nowRule.item(index) : null;
}
/**
* 检测规则的选择符 是否匹配正则
* 用于判断当前规则是否你期望的样式选择符
* @param {String} reg
* @param {number} index
* @returns {Boolean}
*/
matchRule(reg, index) {
if (typeof reg == 'string') reg = new RegExp(reg);
const nowRule = this.ruleItem(index);
if (nowRule) {
return reg.test(nowRule.selectorText);
}
return !1;
}
/**
* 获取规则中的 css对象列表
* @param {number} index
* @param {CSSStyleRule|undefined} cssRule
* @param {Boolean|undefined} bool
* @returns {JSON} 样式变量集合
*/
getRule(index, cssRule) {
const nowRule = this.ruleItem(index, cssRule);
return nowRule ? this.rulesData(nowRule) : {};
}
/**
*
* @param {CSSStyleRule} nowRule
* @param {Boolean|undefined} bool
* @returns {JSON}
*/
rulesData(nowRule, bool) {
let result = {};
let style = nowRule.style;
let index = 0;
while (!0) {
let data = style && style.item(index);
if (!data) break;
index++;
result[data] = style.getPropertyValue(data);
}
if (bool && nowRule && nowRule.cssRules.length) {
let subRules = this.AllRules(nowRule, bool);
if (subRules && subRules.length) {
result.Rules = subRules;
}
}
return result;
}
/**
* 遍历所有规则
* @param {CSSStyleRule} cssRule
* @param {Boolean|undefined} bool
* @returns {array<JSON>}
*/
AllRules(cssRule, bool) {
var result = [];
var index = 0;
while (!0) {
let newSheet = this.ruleItem(index, cssRule);
if (!newSheet) break;
index++;
result.push({
[newSheet.selectorText]: this.rulesData(newSheet, bool)
});
}
return result;
}
/**
* 覆盖CSS规则内容
* @param {String} cssValue cssc值非规则
* @param {number|undefined} index
* @param {CSSStyleRule|undefined} cssRule
*/
putCssText(cssValue, index, cssRule) {
const nowRule = this.ruleItem(index, cssRule);
if (nowRule) {
nowRule.style.cssText = cssValue;
}
}
/**
* 增加CSS规则内容
* @param {String} cssValue cssc值非规则
* @param {number|undefined} index
* @param {CSSStyleRule|undefined} cssRule
* @returns {number}
*/
addCssText(cssValue, index, cssRule) {
const nowRule = this.ruleItem(index, cssRule);
if (nowRule) {
nowRule.style.cssText += cssValue;
}
}
/**
* 重置CSS规则内容
* @param {number} index
* @param {CSSStyleRule|undefined} cssRule
*/
emptyCssText(index, cssRule) {
const nowRule = this.ruleItem(index, cssRule);
if (nowRule) {
nowRule.style.cssText = '';
}
}
/**
* 检测字体支持情况
*/
FontMime = ['woff2', 'woff', 'svg','eot','truetype'].filter(v => CSS.supports('font-format(' + v + ')'));
/**
* 支持字体格式
*/
FontsType = this.FontMime[0];
/**
* 支持字体格式
*/
FontExt = this.FontsType=='truetype'?'ttf':this.FontsType;
/**
* 图标样式
*/
iconFont = "ch-icon";
/**
* 字体Map设置
* @returns {FontFaceSet}
*/
get fonts(){
return document.fonts;
}
/**
* @returns {string[]} 返回已加载字体列表
*/
get fontList() {
return Array.from(this.fonts.keys(), fontFace => fontFace.family);
}
/**
* @method 写入一个字体
* @example addFont('ch-icon','font/ch-icon.woff2',{"style": "normal","weight":"normal"});
* @param {string} name
* @param {string|ArrayBuffer|Blob} url
* @param {Type} options
*/
async addFont(name, url, options,type) {
if (typeof url !='string') url = await this.toURL(url,'font/'+(type||'woff2'));
this.fonts.add(await (new FontFace(name, "url(" + url + ")", options || {})).load());
}
/**
* 返回一个字体图标 基本CSS
* @param {*} name
* @param {*} data
* @returns
*/
fontFace(name, data) {
let rule = 'font-family:' + name;
if (data) {
rule += ';content:' + data;
}
return rule;
}
/**
* 字体图标样式映射
*/
fontsIcon = {};
/**
* 字体图标前缀
*/
fontPrefix = ".cherry-btn-";
/**
* 为字体图标注册
* @param {string} name
* @param {string} data "\exxxx" or "url(xx.svg)"
* @param {string} type "before" "after" ":marker"(ul->li序列list-style)
* @param {string} iconFont "字体名"
*/
addIcon(name, data, type = 'before', iconFont) {
this.fontsIcon[name] = this.fontPrefix.slice(1) + name;
if (!iconFont) iconFont = this.iconFont;
this.insertRule(`${this.fontsIcon[name]}:${type}{${this.fontFace(iconFont, data)}}`);
return this.fontsIcon[name];
}
/**
* 返回样式类名
* @param {string} name
* @returns
*/
getIcon(name) {
return this.fontsIcon[name];
}
/**
* 通用Ajax请求
*@example ajax({hook:'unploadImage',post:{'file':File}})
*@param {JSON} ARG
*@param {string} ARG.url 请求地址
*@param {string} ARG.hook 菜单插件标识
*@param {string} ARG.type 返回类型 默认text,可选xml,head,blob,arrayBuffer
*@param {Function} ARG.success HTTP:200 回调函数
*@param {Function} ARG.progress 上传/下载进度 回调函数
*@param {Function} ARG.error 请求失败或者非HTTP200状态
*@param {JSON} ARG.headers 设置请求头
*@param {any} ARG.paramsDictionary 其他
*@param {string|JSON} ARG.post POST数据
*@param {string|JSON} ARG.params 额外GET参数
*@param {string|JSON} ARG.json 给服务端发送json文件
*@returns {Promise}
* 例如图片上传
* const img = new File([blob],'非常重要的文件名,jpg',{type:'image/jpeg'});
* ajax({
* hook:'image',
* post:{'upload':img},
* success(result){
* //上传成功
* if(result.ok)fn(result.url);
* else fn('remove');
* },
* progress(type,loaded,total){
* if(type=='upload')fn('进度'+Math.floor(100*loaded/tital) + '%')
* }
* });
* PHP
* if(isset($_SEVER['HTTP_CHERRY_HOOK'])){
* header('Content-type: application/json');
* if($_SEVER['HTTP_CHERRY_HOOK']=='image'){
* echo '{"ok":true,"url":"...."}';
* }
* }
*/
ajax(ARG) {
var {
url,
hook, //插件名
type, //定义返回类型 留空则被认为自动.
success, //回调函数
progress, //进度函数
headers = {}, //设置请求头,以前get?name=xxx去实现ajax无刷新,现在客户端无法复制隐式请求
error, //失败函数
paramsDictionary,
post, //POST值
params, //地址GET参数
//外国某些网站喜欢post一个一个JSON文件 而不是传统的POST:key=>value,例如github的API
json,
} = ARG || {};
return new Promise(async resolve => {
const request = new XMLHttpRequest(paramsDictionary);
var resultHead = {};
var resultType = '';
request.addEventListener('readystatechange', event => {
const readyState = request.readyState;
if (readyState === XMLHttpRequest.HEADERS_RECEIVED) {
resultHead = this.getHeader(request);
resultType = (resultHead['content-type'] || 'text/html').split(';')[0];
if (!type) type = resultType.split('/')[1].trim();
switch (type) {
case 'head':
//终止下载
request.abort();
break;
case 'u8':
case 'buf':
request.responseType = 'arrayBuffer';
break;
case 'html':
request.responseType = 'document';
break;
case 'json':
case 'xml':
request.responseType = type;
break;
case 'file':
case 'blob':
case 'javascript':
request.responseType = 'blob';
break;
case 'js':
case 'text':
request.responseType = 'text';
break;
default:
request.responseType = /(text|javascript|ini)/.test(resultType) ? 'text' : 'blob';
break;
}
} else if (readyState === XMLHttpRequest.DONE) {
if (type == 'head') {
success && success.call(request, resultHead);
return resolve(resultHead);
}
let result = request.response;
if (result instanceof Blob) {
let filename = url.split('/').pop();
if (resultHead['disposition']) {
let attachname = resultHead['disposition'].match(/^attachment;\s*filename=[\"\']+?(.+)[\"\']+?$/i);
if (attachname && attachname[1]) filename = decodeURI(attachname[1]);
}
result = new File([result], filename, {
type: resultType
});
} else if (result instanceof ArrayBuffer) {
result = new Uint8Array(result);
}
if (request.status == 200) {
success && success.call(request, result, resultHead);
return resolve(result);
} else {
error && error.call(request, request.statusText, resultHead, result);
return resolve(null);
}
} else if(readyState === XMLHttpRequest.UNSENT){
if (type == 'head') {
success && success.call(request, resultHead);
return resolve(resultHead);
}
return resolve(null);
}
});
/*
request.addEventListener('error',e=>{
error&&error.call(request,request.statusText);
return resolve(null);
});
*/
request.addEventListener('progress', e => {
progress && progress.call(request, 'request', e.loaded, e.total)
});
request.upload.addEventListener('progress', e => {
progress && progress.call(request, 'upload', e.loaded, e.total)
});
if (!url) url = location.href;
if (params) {
var [href, search] = url.split(/\?/);
search = new URLSearchParams(search);
(new URLSearchParams(params).forEach((v, k) => {
search.set(k, v);
}));
search = search.toString();
url = href + (search ? '?' + search : '');
}
headers = Object.assign({
'ajax-fetch': 'cherry'
}, headers || {});
if (json) {
if(typeof json == 'string'||json.constructor===Object){
post = typeof json == 'string' ? json : JSON.stringify(json);
// 告诉客户端这是json文件<字符>
headers['accept'] = 'application/json';
}else{
post = json;
// 告诉客户端这是二进制文件
headers['accept'] = json.type?json.type:'application/octet-stream';
}
} else if (post&&post.constructor !== FormData) {
if(typeof post == 'string'){
post = document.querySelector(post);
}
if (post.constructor === Object) {
let newpost = new FormData();
Object.keys(post).forEach(name => {
newpost.set(name, post[name]);
});
post = newpost;
} else if (post.constructor === HTMLFormElement) {
post = new FormData(post);
}else{
post = null;
}
}
if (hook) {
headers['cherry-hook'] = hook;
}
request.open(post ? 'POST' : 'GET', url);
Object.keys(headers).forEach(name => {
request.setRequestHeader(name, headers[name]);
});
request.send(post);
});
}
/**
* 加载JS/css至浏览器
* @param {*} url
* @param {*} iscss
* @param {*} cache
* @returns {Element}
*/
addJS(url,iscss,cache){
return new Promise(async resolve=>{
const type = 'text/'+(iscss?'css':'javascript');
if(typeof url != 'string' || cache != undefined){
url = this.toURL(url,type);
}else if(cache){
url = this.toURL(await this.getStore('libjs').ajax(url,cache),type);
}
Object.assign(document.head.appendChild(document.createElement(iscss?'link':'script')),{
type,
href: url,
src: url,
rel: StyleSheet.name,
crossorigin: "anonymous",
onload(e) {
resolve(this);
},
onerror(e) {
this.remove();
resolve(null);
}
});
});
}
/**
*
* @param {XMLHttpRequest} request
* @returns {Type}
*/
getHeader(request) {
var headers = request.getAllResponseHeaders();
return headers ? Object.fromEntries(Array.from(headers.trim().split(/\n+/), line => Array.from(line.split(/:\s+/), t => t.trim()))) : {};
}
/**
* 文件上传 iphone无法指定那些文件类型MIME.
* 但是限制图片仍可以用image/*,非图片只能留空不能指定非图片
* @param {*} fn 回调函数
* @param {*} Accept
* @param {*} multiple
* @returns
*/
upload(fn, Accept, multiple) {
let input = document.createElement('input');
input.type = 'file';
if (Accept) input.accept = Accept;
if (multiple) input.multiple = !0;
input.onchange = async e => {
await Promise.all(Array.from(e.target.files, fn));
input.remove();
};
input.click();
return input;
}
/**
* indexdb 数据库名
*/
storeName = "cherry_data";
/**
* 数据库默认表信息
*/
storeMap = {
/**
* 缓存字体
*/
"libjs": {},
/**
* 缓存字体
*/
"fonts": {},
/**
* 缓存字体 在本地未上传时缓存为blob:http
* 导出时替换对应地址为base64
*/
"files": {"type":false},
/**
* 草稿 备份或者缓存
*/
"temp":{"timestamp":false}
};
/**
* 实例化的表
*/
storeList = {};
/**
*
* @param {*} table
*/
getStore(table) {
if (!this.storeList[table]) {
this.storeList[table] = new class indexStore {
constructor(utils, table) {
/**
* @type {IDBDatabase.name}
*/
this.Name = utils.storeName;
/**
* @type {objectStoreNames}
*/
this.table = table;
Object.defineProperties(this, {
transaction: {
/**
*
* @param {*} ReadMode
* @returns {Promise<IDBObjectStore>}
*/
value(ReadMode) {
return utils.transaction(this.table, ReadMode);
}
},
utils: {
get() {
return utils;
}
}
});
}
/**
* 获取表中单元数据
* @param {*} name 键
* @returns
*/
async getItem(name) {
const objectStore = await this.transaction(!0);
return await this.toResult(objectStore.get(name));
}
/**
* 写入表数据单元
* @param {*} name 键
* @param {*} data 数据
* @returns
*/
async setItem(name, data) {
const objectStore = await this.transaction();
return await this.toResult(objectStore.put(data, name));
}
/**
* 删除表数据单元
* @param {*} name 键
* @returns
*/
async removeItem(name) {
const objectStore = await this.transaction();
return await this.toResult(objectStore.delete(name));
}
/**
* 表单元中内容
* @param {string|undefined} name 键
* @param {*} version 版本
* @returns
*/
async getData(name, version) {
const result = await this.getItem(name);
if (result) {
if (result.constructor instanceof Object) {
if (!version || version && result.version == version) {
return result.contents;
}
} else {
return result;
}
}
return null;
}
/**
* 写入表中内容
* @param {string|undefined} name 键
* @param {*} data 内容
* @param {*} version 版本
*/
async setData(name, data, version) {
const result = { contents: data, timestamp: new Date() };
if (version){
if(version.constructor === Object){
Object.assign(result,version);
}else{
result.version = version;
}
}
await this.setItem(name, result);
}
toResult(request){
return new Promise(back=>request.onsuccess = e=>back(request.result))
}
async toCursor(request,fn){
return new Promise(back=>{
request.onsuccess = async e=>{
if(request.result){
await fn(request.result);
request.result.continue();
}else{
back(!0);
}
};
});
}
async getIndex(index){
const objectStore = await this.transaction(!0);
if (index && objectStore.indexNames.contains(index)) {
return objectStore.index(index);
}
return objectStore;
}
/**
* 获取表里所有数据
* @param {string|undefined} index
* @param {IDBKeyRange|undefined} range
* @param {Boolean|undefined} bool
* @returns {JSON|Array}
*/
async all(index, range,bool) {
const objectStore = await this.getIndex(index);
const request = objectStore.openCursor(range);
const result = {};
await this.toCursor(request,data=>{
if(bool&&data.value[index]){
result[data.primaryKey] = data.value[index];
}else if(!bool){
result[data.primaryKey] = data.value;
}
});
return result;
}
/**
* 或者表中所有键值
* @param {string|undefined} index
* @param {IDBKeyRange|undefined} range
* @returns {Array}
*/
async keys(index,range){
const objectStore = await this.getIndex(index);
const request = objectStore.openKeyCursor(range);
const result = [];
await this.toCursor(request,data=>{
result.push(data.primaryKey);
});
return result;
}
/**
* 请求远程数据并且缓存到indexdb
* @param {string} url
* @param {string} name
* @param {*} version
* @returns
*/
async ajax(url, name, version) {
if (!version) {
// https://unpkg.com/browse/[email protected]/dist/mermaid.min.js
let ver = url.match(/@([\d\.]+)/);
if (ver && ver[1]){
version = ver[1];
if (!name) name = url.split('/').pop();
}
}
if (!name) name = url;
let contents = await this.getData(name, version);
if (!contents) {
contents = await this.utils.ajax({ url });
if (contents instanceof Document) {
contents = contents.body.innerHTML;
}
this.setData(name, contents, version);
}
return contents;
}
}(this, table);
}
return this.storeList[table];
}
/**
* 数据库对象
* @param {*} upgrad
* @returns {Promise<IDBDatabase>}
*/
async openStore(upgrad) {
return new Promise(resolve => {
const req = indexedDB.open(this.storeName, this.storeVersion);
req.addEventListener("upgradeneeded", upgrad || (e => {
/**
* @type {IDBDatabase}
*/
let db = e.target.result;
Object.keys(this.storeMap).forEach(tableName => {
if (!db.objectStoreNames.contains(tableName)) {
var storeObj = db.createObjectStore(tableName);
if (this.storeMap[tableName]) {
Object.keys(this.storeMap[tableName]).forEach(index => {
storeObj.createIndex(index, index, this.storeMap[tableName][index] || { unique: false });
});
}
}
});
}), { once: !0 });
req.addEventListener('success', async (e) => {
/**
* @type {IDBDatabase}
*/
let db = e.target.result;
this.storeVersion = db.version + 1;
let tables = Object.keys(this.storeMap).filter(tableName => !db.objectStoreNames.contains(tableName));
if (tables.length > 0) {
db.close();
return resolve(this.openStore(upgrad));
}
return resolve(db);
}, { once: !0 });
});
}
/**
* 数据事务
* @param {string} table
* @param {Boolean|undefined} ReadMode
* @returns {Promise<IDBObjectStore>}
*/
async transaction(table, ReadMode) {
if (!this.storeDB) this.storeDB = this.openStore();
let db = await this.storeDB;
if (!db.objectStoreNames.contains(table)) {
if (!this.storeMap[table]) this.storeMap[table] = {};
db.close();
this.storeDB = this.openStore();
db = await this.storeDB;
}
const transaction = db.transaction([table], ReadMode ? undefined : "readwrite");
return transaction.objectStore(table);
}
/**
* 返回一个BASE64 URL
* @param {*} buf
* @returns
*/
async toBase64(buf,type){
const blob = buf instanceof Blob?buf:new Blob([buf],{type});
const reader = new FileReader();
return new Promise(resolve=>{
reader.onload = e=>{
resolve(reader.result);
}
reader.readAsDataURL(blob);
});
}
async toWebp(file,type,quality){
const img = await createImageBitmap(file);
return new Promise(resolve=>{
const canvas = document.createElement('canvas');
if(!type)type = 'image/webp';
canvas.setAttribute('width',img.width);
canvas.setAttribute('height',img.height);
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
img.close();
canvas.toBlob(blob => {
canvas.remove();
resolve(new File([blob],file.name.replace(/\w+$/,type.split('/').pop(),{type})))
},type, quality===undefined?1:quality);
});
}
/**
* 创建blob临时URL
* @param {Blob|File|ArrayBuffer} buf
*/
async toURL(buf,type) {
return URL.createObjectURL(buf instanceof Blob ? buf : new Blob([buf], {type}));
}
}

