cherry-markdown icon indicating copy to clipboard operation
cherry-markdown copied to clipboard

[建议]尽可能的利用css去计算?

Open nenge123 opened this issue 2 years ago • 18 comments

  • 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好用,特别自适应方面。

nenge123 avatar Nov 18 '23 04:11 nenge123

屏幕截图(15)

nenge123 avatar Nov 19 '23 13:11 nenge123

br9 不如在css中增加 style="--br-line:9" 这样CSS中可以直接根据值height:calc(var(--br-line) * .5rem) 屏幕截图(16)

nenge123 avatar Nov 19 '23 13:11 nenge123

屏幕截图(15)

可以提供最小可复现的代码以便排查问题。

lyngai avatar Nov 19 '23 15:11 lyngai

br9 不如在css中增加 style="--br-line:9" 这样CSS中可以直接根据值height:calc(var(--br-line) * .5rem) 屏幕截图(16)

9 不参与高度计算。

lyngai avatar Nov 19 '23 15:11 lyngai

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

nenge123 avatar Nov 20 '23 00:11 nenge123

!!#000000 !!!#f9cb9c 又更新啦呀,666 !!!!!
!!#000000 !!!#cc0000 !32 又更新啦呀,666!!!!!!

字体背景色,当我先把字体改成更大时,再增加背景色.背景色就有问题.

以前没有文字阴影,但是现在有了,我认为背景色改成text-shadow效果更好,更能体现"文字背景色"或者更有强调效果而不影响美观.

@sunsonliu

nenge123 avatar Nov 20 '23 00:11 nenge123

可以提供最小可复现的代码以便排查问题。

src里面没有源代码。不过通过搜索,空白时,是有Js指定min-height:57px

关键字
display.sizer.style.minHeight = .docHeight
display.heightForcer.style.top

nenge123 avatar Nov 20 '23 00:11 nenge123

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

项目当前还没有类似的需求场景,其实 data-sign 跟 css 或样式没有任何联系哈。如果遇到了实际问题可以提供具体的场景以及最小可复现的源码,我们来评估具体的场景。比如,上面提到的布局问题。

另外,关于css 变量化这一点是在规划内的,但目前仅针对主题色有规划变量化。

lyngai avatar Nov 20 '23 02:11 lyngai

可以提供最小可复现的代码以便排查问题。

src里面没有源代码。不过通过搜索,空白时,是有Js指定min-height:57px

关键字 display.sizer.style.minHeight = .docHeight display.heightForcer.style.top

那就是 codemirror 的源码了

lyngai avatar Nov 20 '23 02:11 lyngai

项目当前还没有类似的需求场景,其实 data-sign 跟 css 或样式没有任何联系哈。如果遇到了实际问题可以提供具体的场景以及最小可复现的源码,我们来评估具体的场景。比如,上面提到的布局问题。

另外,关于css 变量化这一点是在规划内的,但目前仅针对主题色有规划变量化。

sign是标记嘛,我知道没联系。只不过是举例罢了。

例如data-lines额外添加css属性“--lines:”,当然比较推荐的是大于2(n-1)添加--br-line:这样连class="hight-line"都可以省略,进一步减少代码的css类使用。

另外导出Html应该过滤掉data-sign data-lines不必要的代码,这样对数据库储存剩下那么一点小空间。

nenge123 avatar Nov 20 '23 02:11 nenge123

可以提供最小可复现的代码以便排查问题。

src里面没有源代码。不过通过搜索,空白时,是有Js指定min-height:57px

关键字 display.sizer.style.minHeight = .docHeight display.heightForcer.style.top

那就是 codemirror 的源码了

一半一半吧,因为他是初始化时检查高度,而你主容器是height:100%。所以无效高度。解决方法就是flex竖排,还有里面居然用了position fixed,这样会挡住其他下拉框。除了菜单用绝对定位,其他布局适应用flex比起固定定位更好一点吧?

image

nenge123 avatar Nov 20 '23 03:11 nenge123

可以提供最小可复现的代码以便排查问题。

src里面没有源代码。不过通过搜索,空白时,是有Js指定min-height:57px

关键字 display.sizer.style.minHeight = .docHeight display.heightForcer.style.top

那就是 codemirror 的源码了

一半一半吧,因为他是初始化时检查高度,而你主容器是height:100%。所以无效高度。解决方法就是flex竖排,还有里面居然用了position fixed,这样会挡住其他下拉框。除了菜单用绝对定位,其他布局适应用flex比起固定定位更好一点吧?

position:fixed 只有全屏模式才会使用到的,其他应用到 fixed 定位的都是一些浮窗元素。还是建议提供一下最小可复现的代码。

lyngai avatar Nov 20 '23 03:11 lyngai

!!#000000 !!!#f9cb9c 又更新啦呀,666 !!!!!
!!#000000 !!!#cc0000 !32 又更新啦呀,666!!!!!!

字体背景色,当我先把字体改成更大时,再增加背景色.背景色就有问题.

以前没有文字阴影,但是现在有了,我认为背景色改成text-shadow效果更好,更能体现"文字背景色"或者更有强调效果而不影响美观.

@sunsonliu

收到,不过这里对“文字背景色”的理解可能会有出入,一般用户(尤其是没有css背景的用户)会把这个功能理解成“突出显示”(如下图),并且用户反馈也的确是需要一个“能突出某些内容”的功能,这里为了避免产生歧义,我们跟其他编辑器对齐,把“背景颜色”改名成“突出显示”吧

image

然后 背景色和文字大小冲突的问题,我们也跟进下,感谢反馈~

sunsonliu avatar Nov 20 '23 05:11 sunsonliu

然后 背景色和文字大小冲突的问题,我们也跟进下,感谢反馈~ 我觉得背景色完全没必要,因为现在不是WEB2.0那个 <font> 时代,LCD屏幕没有CRT那种色彩逼真还原程度,看起来特别难看.因为替换成阴影色就顺眼很多.

nenge123 avatar Nov 20 '23 05:11 nenge123

继续说说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();
    });

nenge123 avatar Nov 20 '23 05:11 nenge123

继续说说CSS变量化可行性(非主题配色方向) 简化的事件处理. @sunsonliu @lyngai

事实上,这种做法并没有真正做到事件处理的简化,这里有点为了使用技巧而使用技巧的意思在了,反而增加了用户问题排查成本与开发成本。

transition 系列事件为例,目前项目中并没有遇到需要等待 transition 结束才能做的事情。一个菜单按钮点击的事件流也并没有复杂到需要操纵 css 变量才能完成,引入 eventbus 也能达到想要的效果。项目秉承的一个原则是 css 尽量只处理样式和布局相关的逻辑,不过多涉及通过 js 正常的事件流就能完成的逻辑。

综上,以上的建议在目前的项目场景看来意义不大,建议您还是提出基于实际遇到问题的场景,我们才好去评估是否落地这些 tricks。

lyngai avatar Nov 20 '23 07:11 lyngai

事实上,这种做法并没有真正做到事件处理的简化,这里有点为了使用技巧而使用技巧的意思在了,反而增加了用户问题排查成本与开发成本。

transition 系列事件为例,目前项目中并没有遇到需要等待 transition 结束才能做的事情。一个菜单按钮点击的事件流也并没有复杂到需要操纵 css 变量才能完成,引入 eventbus 也能达到想要的效果。项目秉承的一个原则是 css 尽量只处理样式和布局相关的逻辑,不过多涉及通过 js 正常的事件流就能完成的逻辑。

综上,以上的建议在目前的项目场景看来意义不大,建议您还是提出基于实际遇到问题的场景,我们才好去评估是否落地这些 tricks。

也是的,最初我是想利用CSS3的特色平滑替代setTimeOut,菜单出现时更有动态感觉.不过想想也是花里胡巧的,没啥实际意义.

hidden代替display:none 应该可以进行吧? 也好判断,不用重新显示的时候赋值 block

nenge123 avatar Nov 20 '23 13:11 nenge123

@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}));
  }
}

nenge123 avatar Nov 25 '23 13:11 nenge123