Giter VIP home page Giter VIP logo

front-end-quiz's Introduction

前端小测答题收集

收集、整理自己在 zhangxinxu/quiz 上的答题

答题积分排行榜


下述目录按时间倒序排列

2019 下半学期

  1. JS基础测试42:emoji 字符处理视频答疑

  2. CSS基础测试16:两栏垂直居中布局视频答疑

  3. DOM基础测试41:手机号码输入体验优化视频答疑

  4. JS基础测试41:手机号字符处理视频答疑

  5. CSS基础测试15:网格导航布局视频答疑

  6. DOM基础测试40:列表点击、方向键操作视频答疑

  7. JS基础测试40:字符串格式书写方式转换视频答疑 | 测试页面地址

  8. CSS基础测试14:steps - 步骤条视频答疑

  9. DOM基础测试39:缩略图查看与拖动排序视频答疑

  10. JS基础测试39:HEX 颜色处理和伪随机色视频答疑 | 测试页面地址

  11. CSS基础测试13:GitHub Repo 工具条视频答疑

  12. DOM基础测试38:表单元素伪类选择器视频答疑

  13. JS基础测试38期:SVG 字符串处理视频答疑

  14. CSS基础测试12:图片角标视频答疑(二和一)

  15. DOM基础测试37:<dialog> 元素视频答疑(二合一)

  16. JS基础测试37期:数值格式处理/单位换算视频答疑

  17. CSS基础测试11:列表式卡片布局视频答疑

  18. DOM基础测试36:textarea DOM操作视频答疑

  19. JS基础测试36期:图片异步上传视频答疑

  20. CSS基础测试10:纯 CSS 实现下拉菜单视频答疑

  21. DOM基础测试35:表单元素视频答疑

  22. JS基础测试35期:URL传值操作视频答疑 | 单元测试

  23. DOM基础测试34:链接元素的选取与操作视频答疑

  24. CSS基础测试9:文本标题与标签排版视频答疑

  25. JS基础测试34期:数组基本操作视频答疑

2019 上半学期

  1. CSS基础测试8:根据数量自适应宽度布局视频答疑

  2. JS基础测试33:自定义 toFixed 方法 (未录播)

  3. CSS基础测试7:紧贴底部的页脚视频答疑

  4. DOM基础测试32:长按与范围选取视频答疑

  5. JS基础测试32:cookie 及 localStorage 操作视频答疑

  6. CSS基础测试6:音频播放器UI视频答疑

  7. DOM小测31期:DOM伪类选择器视频答疑

  8. JS基础测试31:种子随机数视频答疑

  9. CSS基础测试5:等比缩放正方形布局视频答疑 | CSS Grid布局参考文章 | CSS Flex布局参考文章

  10. DOM基础测试30:滑动条视频答疑

  11. JS基础测试30:字符串长度判断视频答疑

  12. CSS基础测试4:聊天界面布局视频答疑

  13. DOM基础测试29:closest 和 closestAll 方法的实现视频答疑

  14. CSS基础测试3:登录页HTML结构tabindex参考文章 | fieldset, legend元素

  15. JS基础测试28:数组项位移、互换位置视频答疑

  16. DOM基础测试27:返回顶部文章答疑

  17. JS基础测试27:文本校验文章答疑

  18. CSS基础测试1:卡片式布局文章答疑

front-end-quiz's People

Contributors

wingmeng avatar

Watchers

 avatar

front-end-quiz's Issues

JS基础测试34期:数组基本操作

题目:

image


我的回答:

第 1 题:arr 的长度是多少?

解析:数组中的空元素 (empty 元素) 也会被算到数组长度中

console.log(arr.length);  // 4

第 2 题:去除 arr 中的空数组项

解析:数组中的 empty 元素不会参与数组项遍历,故只需返回 true 即可过滤掉 empty 元素(而不会牵连 0NaNnullundefined'' 这些)

arr = arr.filter(it => true);
console.log(arr);  // [1, 2, 3]

第 3 题:写出表达式运行结果

解析:parseInt 接收 stringradix 两个参数,前者是待转换的字符串,后者是进制参考基数,默认是 10。当 parseInt 作为 map 方法的回调函数时,parseInt 的 string 的实参是数组项,radix 的实参是数组索引值,故执行过程如下:

parseInt(1, 0);  // 1. `radix` 为 0 时会自动转换成 10,故输出 1
parseInt(2, 1);  // 2. `radix` 值非法(没有一进制),无法解析,故输出 NaN
parseInt(3, 2);  // 3. `string` 为 3 ,`radix` 为 2,无法将 3 按二进制解析,故输出 NaN
let arr2 = arr.map(parseInt);
console.log(arr2);  // [1, NaN, NaN]

第 4 题:arr 和 arr2 合并成数组 arr3,兼容 IE8+

var arr3 = arr.concat(arr2);
console.log(arr3);  // [1, 2, 3, 1, NaN, NaN]

第 5 题:去除 arr3 中重复内容

解析:利用 ES6 中的 Set 集合不存在重复项的特点来去重

arr3 = [...new Set(arr3)];
console.log(arr3);  // [1, 2, 3, NaN]

JS基础测试30:字符串长度判断

题目:

image


我的答案:

没有什么字符串处理是用一个正则表达式解决不了的,如果有,那就用两个,事实上我用了四个。

DEMO 更精彩 (已封装,并提供测试用例)


第 1 题

这是送分题吗?

content.length > 140;

第 2 题

使用正则将所有连续空格、换行替换为1个

content.trim()  // 移除两端空格
  .replace(/[\r\n]+/g, '\n')  // 连续换行合并为 1 个
  .replace(/[ ]+/g, ' ')  // 内容中的连续空格合并成 1 个
  > 140;

第 3 题

原理:将内容按 2 个 ASCⅡ 字符作为切割点划分数组,累加统计数组项里面的字符数,再加上数组长度 - 1 的值,得到最终字数

var arr = content.trim()
  .replace(/[\r\n]+/g, '\n')
  .replace(/[ ]+/g, ' ')
  .split(/[\x00-\xff]{2}?/g);  // ASCⅡ 码范围(惰性匹配)
var len = arr.length - 1 + arr.reduce(function(total, cur) {
  return total += cur.length	
}, 0);
len > 140;

第 4 题

先按题 2 处理,然后使用正则匹配网址并替换为 10 个占位字符,最后按题 3 处理,统计最后字数

function fixASC2CharsNum(str) {
  var arr = str.split(/[\x00-\xff]{2}?/g);

  return arr.length - 1 +
    arr.reduce(function(total, cur) {
      return total += cur.length;
    }, 0);
}

content = content.trim()
  .replace(/[\r\n]+/g, '\n')
  .replace(/[ ]+/g, ' ');

var regex_url = /https?:\/\/(:?[\w-]+\.)+[\w-]+\S+\b/g;  // 网址(宽松判断)
var shortUrlCharsNum = 10;
var placeholder = Array(shortUrlCharsNum).fill('囧');

content = content.replace(regex_url, function(matched) {
  // 小于 10 个字符的不使用短网址
  if (fixASC2CharsNum(matched) < shortUrlCharsNum) {
    return matched;
  }

  return placeholder;
});

fixASC2CharsNum(content) > 140;

DOM小测31期:DOM伪类选择器

题目:

image


我的回答:

  1. 'styleSheets'
  2. 'head', 'textContent'
  3. 不会 😢
  4. 不会 😢
  5. 3 (原来 id 还可以这么用)
  6. 不会 😢
  7. ~
  8. not

CSS基础测试8:根据数量自适应宽度布局

题目:

image


我的回答:

> 在线 Demo <

<div class="btn-group">
  <button class="btn btn-danger">按钮</button>
</div>
<div class="btn-group">
  <button class="btn btn-danger">按钮</button>
  <button class="btn btn-danger">按钮</button>
</div>
<div class="btn-group">
  <button class="btn btn-danger">按钮</button>
  <button class="btn btn-danger">按钮</button>
  <button class="btn btn-danger">按钮</button>
</div>
/* 解法1:grid 方案 */
.btn-group {
  display: grid;
  grid-gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
}
.btn-group + .btn-group {margin-top: 15px;}
.btn {
  padding: 6px 0;
  text-align: center;
  color: #666;
  background: transparent;
  border: 1px solid;
  border-radius: 10em;
}
.btn-danger {color: #ef7077;}
/* 解法2:flex 方案 */
.btn-group {
  display: flex;
  margin-left: -.5rem;
  margin-right: -.5rem;
}
.btn-group + .btn-group {margin-top: 15px;}
.btn {
  flex: 1;
  padding: 6px 0;
  margin: 0 .5rem;
  text-align: center;
  color: #666;
  background: transparent;
  border: 1px solid;
  border-radius: 10em;
}
.btn-danger {color: #ef7077;}

JS基础测试41:手机号字符处理

题目:

0
原图

我的回答:

第 1 题

function trimBlank(strTel) {
  return strTel.trim()
}

第 2 题

function doubleByteToSingle(strTel) {
  const doubleByteNums = '0123456789';
  return strTel.replace(
    new RegExp('[' + doubleByteNums + ']', 'g'), matched =>
      doubleByteNums.indexOf(matched)
  )  
}

第 3 题

function rmCountryCode(strTel) {
  return strTel.replace(/^\+8\s*6/, '')
}

第 4 题

function rmConnector(strTel) {
  return strTel.replace(/\D/g, '')
}

第 5 题

function isValidTel(strTel) {
  return /^1\d{10}$/.test(strTel);
}
测试用例
const tests = [
  { it: ' 13208033621  ', expect: '13208033621', handle: trimBlank },
  { it: '13208033621', expect: '13208033621', handle: doubleByteToSingle },
  { it: '+8613208033621', expect: '13208033621', handle: rmCountryCode },
  { it: '1320-8033-621', expect: '13208033621', handle: rmConnector },
  { it: '1320 8033 621', expect: '13208033621', handle: rmConnector },
  { it: ' +8  613 208-033621  ', expect: true, handle: isValidTel }
];

function trimBlank(strTel) {
  return strTel.trim()
}

function doubleByteToSingle(strTel) {
  const doubleByteNums = '0123456789';
  return strTel.replace(
    new RegExp('[' + doubleByteNums + ']', 'g'), matched =>
      doubleByteNums.indexOf(matched)
  )  
}

function rmCountryCode(strTel) {
  return strTel.replace(/^\+8\s*6/, '')
}

function rmConnector(strTel) {
  return strTel.replace(/\D/g, '')
}

function isValidTel(strTel) {
  return /^1\d{10}$/.test(
    rmConnector(
      rmCountryCode(
        doubleByteToSingle(
          trimBlank(strTel)
        )
      )
    )
  );
}

tests.map(test => {
  const result = test.handle(test.it);
  const isPassed = result === test.expect;

  console.group(`${test.it} -> ${test.expect}`);
    isPassed ?
      console.log('%c√ Pass', 'color: green') :
      console.log('%c× Failed! the actual result is: ' + result, 'color: red');
  console.groupEnd();
});

CSS基础测试5:等比缩放正方形布局

题目:

image


我的答案:

直接使用强大的 Grid 布局,妥妥的。> 在线 DEMO <

<ul class="square-box">
  <li class="square-item bg-dark"></li>
  <li class="square-item bg-gray"></li>
  <li class="square-item bg-sienna active"></li>
  <li class="square-item bg-gold"></li>
  <li class="square-item bg-crimson"></li>
  <li class="square-item bg-orchid"></li>
  <li class="square-item bg-silver"></li>
</ul>
/* css reset */
html {font-size: 16px;}
ul {
  padding: 0;
  margin: 0;
  list-style: none;
}

/* 布局 */
.square-box {
  display: grid;
  grid-template-columns: repeat(5, 1fr);  /* 划分 5 个等宽片段 */
  grid-template-rows: 1fr 1fr;
  grid-gap: .5rem .375rem;
}
.square-item {
  padding-bottom: 100%;  /* 容器的 padding 百分比值按其宽度来计算 */
  outline: .15rem solid transparent;
}
.square-item.active {outline-color: #222;}

/* 颜色 */
.bg-dark    {background: #424242;}
.bg-gray    {background: #8c8c8c;}
.bg-sienna  {background: #d48b69;}
.bg-gold    {background: #fecb66;}
.bg-crimson {background: #c54941;}
.bg-orchid  {background: #591c93;}
.bg-silver  {background: #e6e6e6;}

/* 响应式断点(CSS基础测试4),基于 iPhone6 尺寸 */
@media screen and (min-width: 375px) {
  html {
    font-size: calc(100% + 2 * (100vw - 375px) / 39);
    font-size: calc(16px + 2 * (100vw - 375px) / 39);
  }
}
@media screen and (min-width: 414px) {
  html {
    font-size: calc(112.5% + 4 * (100vw - 414px) / 586);
    font-size: calc(18px + 4 * (100vw - 414px) / 586);
  }
}

JS基础测试38期:SVG 字符串处理

题目:

image


我的回答:

第 1 题:

// 方法1:利用 RegExp 的零宽断言
str = str.replace(/fill="(?!none)[^"]+"/gi, '');

// 方法2:先用 RegExp 通用判断,然后 replace 函数进一步判断
str = str.replace(/fill="([^"]+)"/gi, function($0, $1) {
  return $1.toLowerCase() === 'none' ? $0 : '';
});

第 2 题:

接第 1 题

window.btoa(str)

第 3 题:

接第 1 题

var reg_encodeChars = new RegExp('["%#{}<>]', 'g');
str = str.replace(reg_encodeChars, encodeURIComponent);

CSS基础测试9:文本标题与标签排版

题目:

image


我的回答:

首先想到了用 display: table 方案,但几经尝试没 hold 住,后改用 float 实现:> 在线 Demo <

  1. 为了兼容 IE8 下右浮动元素与非浮动元素的对齐,HTML 标签顺序未能与可视效果相一致(界面显示上标题在前,标签在后,而 HTML 代码中标签在前,标题在后);
  2. 第 2 题,宽度不足时会截断标题文本,其他浏览器是从左侧开始截断的(符合题意),而 IE(8~11)下是从右侧开始截断的。
<!-- 1 -->
<ul class="book-list">
  <li class="book-list-item">      
    <div class="book-tags">
      <span class="tag">都市</span>
      <span class="tag" theme="red">连载中</span>
      <span class="tag" theme="blue">54.82万字</span>
    </div>
    <h3 class="book-title">
      <a class="book-title-link" href="##">这次是一个新的故事。浩劫余生,终见光明</a>
    </h3>
  </li>
  <li class="book-list-item">
    <div class="book-tags">
      <span class="tag">都市</span>
      <span class="tag" theme="red">完本</span>
      <span class="tag" theme="blue">1万字</span>
    </div>
    <h3 class="book-title">
      <a class="book-title-link" href="##">穿越天地复苏的平行世界,偶获诸天聊天群</a>
    </h3>
  </li>
  <li class="book-list-item">
    <div class="book-tags">
      <span class="tag">科幻</span>
      <span class="tag" theme="red">完本</span>
      <span class="tag" theme="blue">1059.98万字</span>
    </div>
    <h3 class="book-title">
      <a class="book-title-link" href="##">修真四万年</a>
    </h3>
  </li>
  <li class="book-list-item">
    <div class="book-tags">
      <span class="tag">童话</span>
      <span class="tag" theme="red">完本</span>
      <span class="tag" theme="blue">1万字</span>
    </div>
    <h3 class="book-title">
      <a class="book-title-link" href="##">The quick brown fox jumps over a lazy dog.</a>
    </h3>
  </li>
</ul>

<!-- 2 -->
<ul class="book-list is-inverse">
  <li class="book-list-item">      
    <div class="book-tags">
      <span class="tag">都市</span>
      <span class="tag" theme="red">连载中</span>
      <span class="tag" theme="blue">54.82万字</span>
    </div>
    <h3 class="book-title">
      <a class="book-title-link" href="##">这次是一个新的故事。浩劫余生,终见光明</a>
    </h3>
  </li>
  <li class="book-list-item">
    <div class="book-tags">
      <span class="tag">都市</span>
      <span class="tag" theme="red">完本</span>
      <span class="tag" theme="blue">1万字</span>
    </div>
    <h3 class="book-title">
      <a class="book-title-link" href="##">穿越天地复苏的平行世界,偶获诸天聊天群</a>
    </h3>
  </li>
  <li class="book-list-item">
    <div class="book-tags">
      <span class="tag">科幻</span>
      <span class="tag" theme="red">完本</span>
      <span class="tag" theme="blue">1059.98万字</span>
    </div>
    <h3 class="book-title">
      <a class="book-title-link" href="##">修真四万年</a>
    </h3>
  </li>
  <li class="book-list-item">
    <div class="book-tags">
      <span class="tag">童话</span>
      <span class="tag" theme="red">完本</span>
      <span class="tag" theme="blue">1万字</span>
    </div>
    <h3 class="book-title">
      <a class="book-title-link" href="##">The quick brown fox jumps over a lazy dog.</a>
    </h3>
  </li>
</ul>
.book-list {
  padding: 0;
  margin: 0;
  list-style: none;
}

.book-list-item {
  margin: .8em 0;
  line-height: 1.2;
  font-size: 18px;
}
.book-list-item:after {
  content: "";
  clear: both;
  display: table;
}

.book-title {
  margin: 0;
  font-size: 100%;
  font-weight: 500;
}

.book-title-link {
  display: block;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  text-align: left;
  text-decoration: none;
  color: #9da2a9;
}
.book-title-link:hover {color: #498bed;}

.is-inverse .book-title-link {direction: rtl;}

.book-tags {
  float: right;
  margin-left: 1em;
}    
.tag {
  display: inline-block;
  padding: 3px;
  white-space: nowrap;
  line-height: 1;
  font-size: 82%;
  color: #9da2a9;
  border: 1px solid;
  border-radius: 2px;
}
.tag[theme="red"] {color: #ee4e54;}
.tag[theme="blue"] {color: #498bed;}

DOM基础测试38:表单元素伪类选择器

题目:

image


我的回答:

第 1 题:

document.querySelectorAll('[type=radio]:required');

第 2 题:

// 处于禁用状态的 fieldset 下的 radio 也包含在内
document.querySelectorAll('[type=radio]:disabled');

第 3 题:

document.querySelectorAll('[type=radio]:checked');

第 4 题:

const activateBtn = document.getElementById('removeDisabled');

activateBtn.addEventListener('click', () => {
  const disabledRadios = document.querySelectorAll('[type=radio]:disabled');

  [...disabledRadios].map(radio => {
    const fieldset = radio.closest('fieldset');  // 这里未考虑多层 fieldset 嵌套的情况

    radio.disabled = false;
    fieldset && (fieldset.disabled = false);
  });
});

第 5 题:

纯 CSS 方案即可

[type=radio]:invalid {
  outline: 3px dashed red;
}

DOM基础测试39:缩略图查看与拖动排序

题目:

image


我的回答:

> Demo <

//zxx: 拖不动,除了拖到最后

wingmeng: 多谢张老师,昨晚太困了 js 逻辑没写完 😢 ,今早才补完的

.img-list {
  display: flex;
  flex-wrap: wrap;
  width: 260px;
  background: #fff;
  border: 1px solid #ccc;
  transition: all .2s;
}

.img-list.is-dragenter {
  background: #ffd;
  border-color: #333;
}

.img-list > img {
  max-width: 33.3333%;
  padding: 2px;
  box-sizing: border-box;
  cursor: move;
}

.img-preview img {
  display: block;
  max-width: 100%;
}
.img-preview::backdrop {background: rgba(0, 0, 0, .6);}
<div class="img-list" id="imgList">
  <img src="https://cdn.pixabay.com/photo/2014/12/30/13/19/girls-583917_960_720.jpg" alt="海边女孩">
  <img src="https://cdn.pixabay.com/photo/2019/10/21/10/33/garden-4565700_960_720.jpg" alt="花园">
  <img src="https://cdn.pixabay.com/photo/2019/10/27/18/48/chinatown-4582511_960_720.jpg" alt="都市">
  <img src="https://cdn.pixabay.com/photo/2016/12/03/14/20/woman-1879905_960_720.jpg" alt="金发女郎">
  <img src="https://cdn.pixabay.com/photo/2019/10/08/18/13/matterhorn-4535693_960_720.jpg" alt="山峰">
  <img src="https://cdn.pixabay.com/photo/2017/04/22/10/15/sport-2250970_960_720.jpg" alt="运动">
  <img src="https://cdn.pixabay.com/photo/2013/06/10/09/23/morocco-123978_960_720.jpg" alt="沙漠">
</div>
const viewImgList = {
  init(selector) {
    this.el = document.querySelector(selector);
    this.dialog = null;
    this.bindClick();
    this.bindDrag();
  },
  bindClick() {
    // 图片点击事件
    this.el.addEventListener('click', e => {
      if (e.target.tagName.toLowerCase() === 'img') {
        this.togglePreview(e.target.src);
      }
    });

    // 任意位置点击
    window.addEventListener('click', e => {
      if (!this.el.contains(e.target)) {
        this.togglePreview(false);
      }
    });
  },
  bindDrag() {
    let imgElm = null;
    const isInside = elm => elm === this.el || this.el.contains(elm);

    // 拖动开始
    this.el.addEventListener('dragstart', e => {
      e.target.style.opacity = 0;
      imgElm = e.target;
    });

    // 拖动结束
    this.el.addEventListener('dragend', e => e.target.style.opacity = '');

    // 放置目标
    document.addEventListener('dragover', e => {
      if (isInside(e.target)) {
        e.preventDefault();

        if (e.target === this.el) {
          this.el.appendChild(imgElm);
        } else {
          const compareValue = imgElm.compareDocumentPosition(e.target);

          if (compareValue === 2) {  // 目标图片在前
            e.target.before(imgElm);  // 添加到目标图片前面
          } else if (compareValue === 4) {  // 目标对象在后
            e.target.after(imgElm);  // 添加到目标图片后面
          }
        }
      }
    });

    // 进入目标区域
    document.addEventListener('dragenter', e => {
      this.el.classList[isInside(e.target) ? 'add' : 'remove']('is-dragenter');
    });

    // 允许放置
    this.el.addEventListener('drop', e => {
      e.preventDefault();
      this.el.classList.remove('is-dragenter');
    });
  },
  togglePreview(src) {
    if (src) {
      this.buildDialog(src);
      this.dialog && !this.dialog.open && this.dialog.showModal();          
    } else {
      this.dialog && this.dialog.close();
      this.destoryDialog();
    }
  },
  buildDialog(imgSrc) {
    const dialog = document.createElement('dialog');
    const img = new Image();

    dialog.className = 'img-preview';
    img.src = imgSrc;
    this.dialog = dialog;
    dialog.appendChild(img);
    document.body.appendChild(dialog);
  },
  destoryDialog() {
    this.dialog && document.body.removeChild(this.dialog);
    this.dialog = null;
  }
};

viewImgList.init('#imgList');

DOM基础测试35:表单元素

题目:

image


我的回答:

第 1 题:

document.getElementById('loginForm');  // 方法1:经典的 id 选择器
document.querySelector('#loginForm');  // 方法2:万能的 querySelector
document.forms.loginForm;  // 方法3:表单原生方法,还可以写作:document.forms['loginForm']
loginForm;  // 方法4:标签的 id 可以直接当变量来用

第 2 题:

  1. form 标签添加 onsubmit="return false"

    <form id="loginForm" action="/account/login" method="post" onsubmit="return false">
  2. form 提交事件里 return false(仅限 DOM 0 级)

    loginForm.onsubmit = function() {
      // 其他操作...
      return false;
    };
  3. form 提交事件里阻止表单默认行为 preventDefault

    // DOM 0 级
    loginForm.onsubmit = function(e) {      
      e.preventDefault();
      // 其他操作...
    };
    
    // DOM 2 级
    loginForm.addEventListener('submit', function(e) {
      e.preventDefault();
      // 其他操作...
    }, false);

第 3 题:

const btnTypes = ['button', 'submit', 'reset', 'image'];
const formData = [...loginForm]
  // 过滤掉表单按钮
  .filter(field => !btnTypes.includes(field.type))

  // 将表单元素的值封装成 k-v 形式的数据,例如:{username: 'abc'}
  .reduce((data, { name, value }) => {        
    // 这里仅按照题意作简单处理,
    // 如果要完善的话,还要考虑单选框、复选框这类 name 值相同的表单元素
    data[name] = value;        
    return data;
  }, {});

console.log(formData)

补充:抽空自己封装表单库,意外发现原来根本不用这么折腾,早就有 FormData 这个 JS 原生对象了,它可以将数据编译成键值对,直接供 AJAX 来使用。孤陋寡闻了……

第 4 题:

在 AJAX 提交前为 submit 按钮设置 disabled 属性来禁用按钮(同时也禁用了表单提交),等 AJAX 返回后(无论成功失败)再去掉 disabled 属性启用按钮。

第 5 题:

为隐藏的输入框加上 form="loginForm" 属性即可。
利用这个特性可以把从属于表单的元素放在任何地方,只需指明该元素的 form 特性值为表单 id 即可,这样该元素就从属于表单了。

<input name="from" type="hidden" form="loginForm">

DOM基础测试41:手机号码输入体验优化

题目:

图片暂缺

我的回答:

> 在线 Demo <

第 1 题

function telTrim(str) {
  str = String(str);
  const s = str.replace(/[\s-]/g, '');
  return /^\d{11}$/.test(s) ? s : str;
}

第 2 题

const formElm = document.getElementById('form');
const inputElm = document.getElementById('input');

// 或者
const formElm = document.forms['form'];
const inputElm = formElm['input'];

第 3 题

inputElm.addEventListener('drop', function(e) {
  e.preventDefault();
  this.value = telTrim(e.dataTransfer.getData('text'));
});

第 4 题

inputElm.addEventListener('paste', function(e) {
  e.preventDefault();
  const { selectionStart: start, selectionEnd: end } = this;  // 文本框光标位置
  const text = e.clipboardData.getData('text');

  this.setRangeText(text, start, end);  // 从当前光标位置追加粘贴的内容
  this.value = telTrim(this.value);  // 格式化文本框内容
});

第 5 题

formElm.addEventListener('submit', function(e) {
  e.preventDefault();
  inputElm.value = telTrim(inputElm.value);

  // 后续提交表单的操作... 如 ajax 或 this.submit();
});

JS基础测试31:种子随机数

题目:

image


我的回答:

预期是达到了,就是算法复杂度有点高了…… > 测试 DEMO <

function getRandomSkins(uid, skins) {
  skins = skins instanceof Array ? skins : [];

  if (!uid) {
    return skins;
  }

  // 1.将 uid 的每个字符串元字符转换为编码值,组合成一个数字
  var charCodes = String(uid).split('').reduce(function(total, cur) {
    return total + (isNaN(cur) ? cur.charCodeAt() : cur);
  }, '');

  // 2.使用上一步生成的数字生成一个和皮肤数组长度一致的字符串数字
  var seed = String(Math.sin(charCodes)).split('.')[1].substr(0, skins.length);      

  // 3.将上一步生成的字符串数字打散添加到皮肤数组的每一项中,用于排序
  var result = skins.map(function(item, idx) {
    item._order = Number(seed[idx]);
    return item;
  });

  return (
    // 4.根据 order 排序
    result.sort(function(o, p) {
      return o._order - p._order;
    })
    
    // 5.移除 order,还原数组本来面貌
    .map(function(item) {
      delete item._order;
      return item;
    })
  )
}

JS基础测试35期:URL传值操作

题目:

image

我的回答:

const object1 = {
  userid: 123,
  username: '王二',
  tel: '13208033621'
};

第 1 题:

一句话:两者都会对 URL 中的特殊字符进行编码,区别是两者编码的字符范围不一样,前者不会对属于 URI 的特殊字符进行编码,而后者会。

第 2 题:

Object.keys(object1)
  .map(key => `${key}=${encodeURIComponent(object1[key])}`)  // 转码
  .join('&');

第 3 题:

location.search;

第 4、5 题:

2019-07-25 修改:调整了函数和形参命名,使之更加语义化;增加了测试用例

// zxx: 有bug
function getQueryObj(queryStr = '') {
  // 移除前面的 ? 号(如有)
  queryStr = queryStr.indexOf('?') === 0 ? queryStr.substr(1) : queryStr;
  if (!queryStr) return {};

  return queryStr.split('&')
    .map(s => s.split('='))  // 拆解 key 和 value
    .reduce((obj, [key, value]) => {
      value = value ? decodeURIComponent(value) : '';  // 解码

      if (key in obj) {
        Array.isArray(obj[key]) ? obj[key].push(value) : obj[key] = [obj[key], value];
      } else {
        obj[key] = value;
      }
        
      return obj;
    }, {});
}

getQueryObj 测试用例:

getQueryObj();  // 参数为空

// 一般情况
getQueryObj('userid=123&username=%E7%8E%8B%E4%BA%8C&tel=13208033621');

// 空值(username 值为空)
getQueryObj('userid=123&username=&tel=13208033621');

// 多个重复的键名(tel、favorite)
getQueryObj('?userid=123&tel=13208033621&username=%E7%8E%8B%E4%BA%8C&favorite=rap&favorite=Hip%20hop&favorite=basketball&tel=15888888888&tel=18099999999');

DOM基础测试30:滑动条

题目:

image

.slider {
    padding: 5px 0;
    position: relative;
    margin: 30px 10%;
    --percent: 0;
}
.slider-track {
    display: block;
    width: 100%; height: 6px;
    background-color: lightgray;
    border: 0; padding: 0;
}
.slider-track::before {
    content: '';
    display: block;
    height: 100%;
    background-color: skyblue;
    width: calc(1% * var(--percent));
}
.slider-thumb {
    position: absolute;
    width: 16px; height: 16px;
    border: 0; padding: 0;
    background: #fff;
    box-shadow: 0 0 0 1px skyblue;
    border-radius: 50%;
    left: calc(1% * var(--percent)); top: 0;
    margin: auto -8px;
}

image


我的答案:

> 在线 DEMO <

  1. 支持 PC 端和移动端;
  2. 支持缺省赋值(第 2 题);
  3. 支持键盘操作;
  4. 可使用 js 动态赋值(第 2 题);
  5. 可指定渲染容器,默认渲染到 body 标签最后(第 1 题);
  6. 支持点击“轨道”赋值定位(第 3 题);
  7. 支持滑块拖动赋值(第 4 题)。

使用方式示例

let myslider1 = new Slider({ el: '#box' });  // 指定容器
new Slider({ value: 50 });  // 缺省赋值
new Slider();  // 无参数(插入到 body 标签最后,赋值为 0)

myslider1.val(value);  // js 动态赋值

已封装的 Slider 组件

class Slider {
  constructor(opts = {}) {
    this.el = opts.el;
    this.value = opts.value || 0;
    this.slider = null;
    this.render();
    this.bindEvt();

    return {
      // 赋值方法
      val: (value) => {
        this.val(value);
      }
    }
  }

  // 渲染 DOM
  render() {
    const container = document.querySelector(this.el);
    const slider = document.createElement('div');

    this.slider = slider;

    // 有缺省值则赋值
    if (this.value) {
      this.val(this.value);
    }

    slider.className = 'slider';
    slider.innerHTML = (
      // 轨道无需获取焦点
      `<button class="slider-track" tabindex="-1"></button>
      <button class="slider-thumb"></button>`
    );

    if (container) {
      container.appendChild(slider);
    } else {
      // 若未指定容器,则在 body 标签最后插入 DOM 结构
      document.body.appendChild(slider);
    }
  }

  // 绑定事件
  bindEvt() {
    const { slider } = this;
    const slider_track = slider.querySelector('.slider-track');
    const slider_thumb = slider.querySelector('.slider-thumb');
    let readyMove = false;

    const startHandle = e => {
      if (e.target === slider_thumb) {
        e.stopPropagation();
        readyMove = true;
      }
    };

    const moveHandle = e => {
      if (readyMove) {
        this.computeVal(e);
      }
    };

    const endHandle = () => readyMove = false;

    // 点击监听
    slider.addEventListener('click', e => {
      // 点击轨道
      if (e.target === slider_track) {
        this.computeVal(e);
      }
    }, false);

    // 键盘监听
    slider.addEventListener('keydown', e => {
      // 滑块获得焦点
      if (document.activeElement === slider_thumb) {
        let value = this.val();
        
        switch(e.keyCode) {
          case 37:  // 左箭头
            value--;
            break;
          case 39:  // 右箭头
            value++;
            break;
        }

        this.val(value);
      }
    }, false);

    // 开始拖动
    slider.addEventListener('touchstart', startHandle);
    slider.addEventListener('mousedown', startHandle);

    // 拖动中
    window.addEventListener('touchmove', moveHandle);
    window.addEventListener('mousemove', moveHandle);

    // 拖动结束
    window.addEventListener('touchend', endHandle);
    window.addEventListener('mouseup', endHandle);
  }

  // 计算当前值
  computeVal(e) {
    const { width, left } = this.slider.getBoundingClientRect();
    let posX = e.pageX;

    if (e.touches) {  // 兼容移动端
      posX = e.touches[0].pageX;
    }

    this.val((posX - left) / width * 100);
  }

  // 赋值 & 取值
  val(value) {
    if (typeof value === 'undefined') {
      // 返回当前 slider 的 percent 值
      return this.slider.style.getPropertyValue('--percent').trim() || 0;
    }

    if (isNaN(value)) {  // 过滤非法字符
      return;
    }

    // 边界处理
    if (value < 0) {
      value = 0;
    } else if (value > 100) {
      value = 100;
    }

    this.slider.style.setProperty('--percent', value);
  }
}

JS基础测试42:emoji 字符处理

题目:

image

我的回答:

第 1 题

let title = '这是最后一期了,感谢同学们的一路相伴,更要向为47期小测耗费心血的张老师致敬!';

if (title.length > 15) {
  title = title.replace(/^(.{6}).*(.{6})$/, '$1...$2');
}

console.log(title);  // "这是最后一期...张老师致敬!"

第 2 题

let filename = 'weixin_20191221232646.jpg';

if (filename.length > 15) {
  filename = filename.replace(/^(.+)(\.\w+)$/, (_, $1, $2) => {
    let tail = '...' + $1.substr(-1) + $2;
    return $1.substr(0, tail.length - 1) + tail;
  });
}

console.log(filename);  // "weixin_...6.jpg"

第 3 题

思路基本理顺了,但实现上仍然有 bug,待进一步优化 😂

function formatTitle(titleStr) {
  // emoji 的 unicode 范围实在是太繁杂了,既有双字节的,还有单字节的
  // 下面的 emoji 正则表达式参考自 [email protected]
  const regexp_emoji = new RegExp('(' +
    '(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff])' +
    '[\ufe0e\ufe0f]?' +
    '(?:[\u0300-\u036f\ufe20-\ufe23\u20d0-\u20f0]|\ud83c[\udffb-\udfff])?' +
    '(?:\u200d(?:[^\ud800-\udfff]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff])' +
    '[\ufe0e\ufe0f]?' +
    '(?:[\u0300-\u036f\ufe20-\ufe23\u20d0-\u20f0]|\ud83c[\udffb-\udfff])?)*' +
  ')', 'g');
  const regexp_symbol = /[,;:……——]/;
  const emojiArr = titleStr.match(regexp_emoji);
  // 将 title 字符串打散成数组,避免双字节 emoji 字符的影响(每个 emoji 算1个字符)
  const titleArr = titleStr.split(regexp_emoji).reduce((arr, s) => {
    if (emojiArr && emojiArr.includes(s)) {
      arr.push(s)
    } else {
      arr = arr.concat(s.split(''))
    }

    return arr;
  }, []);

  if (titleArr.length > 15) {
    const half = Math.floor(titleArr.length / 2);
    let left = titleArr.slice(0, half);
    let right = titleArr.slice(half);
    let leftPoint = 0;
    let rightPoint = 0;

    // 从中心向两端查找第一个 emoji 或中文标点出现的指针位置
    left.some((_, idx, arr) => {
      const index = arr.length - 1 - idx;
      if (emojiArr && emojiArr.includes(arr[index]) || regexp_symbol.test(arr[index])) {
        leftPoint = index + 1;
        return true;
      }
    });
    right.some((item, idx) => {
      if (emojiArr && emojiArr.includes(item) || regexp_symbol.test(item)) {
        rightPoint = idx;
        return true;
      }
    });

    if (leftPoint === 0 && rightPoint === 0) {
      return titleStr.replace(/^(.{6}).*(.{6})$/, '$1...$2');
    }

    let diffValue;

    if (rightPoint > left.length - leftPoint) {
      diffValue = 15 - left.length - 3;  // 差值
      left = left.join('');
      right = right.slice(0, rightPoint).join('') + '...' +
        right.slice(right.length - 1 - rightPoint).join('');
    } else {
      diffValue = 15 - right.length - 3;  // 差值
      left = left.slice(0, left.length - 1 - diffValue).join('') + '...' +
        left.slice(leftPoint).join('');
      right = right.join('');
    }

    return left + right;
  }

  return titleArr.join('');
}

JS基础测试36期:图片异步上传

题目:

image


我的回答:

第 1 题:

var xhr = new XMLHttpRequest();

// 上传进度(监听 upload 的 onprogress 事件)
xhr.upload.onprogress = function(e) {
  var percent = e.loaded / e.total * 100
  console.log('上传进度:' + percent + '%');
}

// 请求完成
xhr.onload = function() {
  var resOK = 200;
  console.log('请求完成');

  // 一个完成的请求不一定是成功的请求,故需要判断状态码来确认
  if (xhr.status === resOK) {
    console.log('请求成功')
  }
}

// 请求错误
xhr.onerror = function() {
  console.log('请求失败')
}

xhr.open('POST', '/upload', true);
xhr.send(file);

第 2 题:

var maxFileSize = 1 * (1024 * 1024);  // 1MB

files = [].slice.call(files).filter(function(file) {
  return file.size <= maxFileSize
});

第 3 题:

这里用 Promise.all 方法无疑是最合适的,但看题目中的 js 代码没有使用 ES6+,所以这里用 ES5 规范来实现:

// 这里的 files 是第 2 题中处理过的 files
files.map(uploadImg);

function uploadImg(img) {
  var xhr = new XMLHttpRequest();

  xhr.onload = function() {
    var resOK = 200;

    if (xhr.status === resOK) {
      img._uploaded = true
    }
  }

  xhr.onerror = function() {
    img._uploaded = false
  }

  // 请求结束(无论成功与否)
  xhr.onloadend = checkUpload;

  xhr.open('POST', '/upload', true);
  xhr.send(img);
}

function checkUpload() {
  var isDone = files.every(function(file) {
    return file.hasOwnProperty(_uploaded)
  });

  if (isDone) {
    console.log('上传已结束');

    var result = files.reduce(function(count, cur) {
      count[cur._uploaded ? 'success' : 'fail'] += 1;
      return count;
    }, {
      success: 0,
      fail: 0
    });

    console.log('上传结果:' +
      '成功' + result.success + '个' +
      '失败' + result.fail + '个'
    );

    if (result.success === files.length) {
      console.log('图片全部上传成功')
    }
  }
}

CSS基础测试12:图片角标

题目:

image


我的回答:

> 在线 Demo <

.books {
  display: flex;
  flex-flow: wrap;
}

.book-item {
  margin: 5px;
  text-decoration: none;
}

.book-item > .tag {position: absolute;}

.book-item > img {
  display: block;
  width: 120px;
  height: 160px;
}

.tag {
  display: inline-block;
  padding: 3px 5px;
  box-sizing: border-box;
  line-height: 1.2;
  font-size: 12px;
  color: #fff;
  background: #cd0000;
}

.tag-top {
  width: 28px;
  height: 28px;
  padding: 2px 4px;
  background: linear-gradient(135deg, #cd0000 50%, transparent 0);  
}

.tag-hot {
  padding-right: 12px;
  padding-bottom: 4px;
  background: linear-gradient(114deg, #cd0000 calc(100% - 8px), transparent 0);
}

.tag-number {
  min-width: 26px;
  min-height: 26px;
  padding-right: 11px;
  border-bottom-right-radius: 100%;
}

CSS基础测试3:登录页HTML结构

题目:

image


我的答案:

<div class="panel login-box">
    <div class="panel-head">
        <h3 class="panel-title">登录</h3>
    </div>
    <div class="panel-body">
        <form id="loginForm">
            <div class="form-group">
                <input type="text" class="form-input" placeholder="账号/手机" autofocus required>
            </div>
            <div class="form-group">
                <div class="input-group">
                    <input type="password" class="form-input" placeholder="密码" required>
                    <a href="..." class="input-group-addon">忘记密码?</a>
                </div>
            </div>
            <div class="form-group">
                <div class="input-group">
                    <input type="text" class="form-input" placeholder="验证码" autocomplete="off" required>
                    <a href="..." class="input-group-addon">
                        <img src="..." alt="验证码" id="validCode" width="86" height="28">
                    </a>
                </div>
            </div>
            <p id="tipMsg">&nbsp;</p>
            <div class="form-group">
                <button type="submit" class="btn btn-primary btn-block">登录</button>
                <a href="..." class="btn btn-link btn-block">立即注册</a>
            </div>
        </form>
    </div>
</div>

CSS基础测试6:音频播放器UI

题目:

image

素材图:
image


我的回答:

写之前翻看了下 Chrome 原生 <audio> 的 shadow DOM,有所启发。 > 在线 DEMO <

<div class="audio-controls">
  <div class="audio-controls-inner">
    <button class="audio-controls-button" aria-label="play"></button>
    <span class="audio-controls-time" aria-label="current">0:05</span>
    <span class="audio-controls-time" aria-label="remaining">0:05</span>
    <input class="audio-controls-timeline" type="range" step="60" max="5000" value="5000">
    <button class="audio-controls-button" aria-label="volume">
      <span class="audio-controls-button-volume-icon"><i></i></span>
    </button>
    <button class="audio-controls-button" aria-label="menu"></button>
  </div>		
</div>
body { background: #d6d6d6; }

/********** 主容器 **********/
.audio-controls {   
  /* width: 400px; */
  text-align: left;
}

/********** 样式容器 **********/
.audio-controls-inner {
  font-size: 16px;      /* 控件尺寸 */
  color: #000;          /* 控件主题色 */
  background: #f1f3f4;  /* 控件背景色 */

  display: flex;
  align-items: center;
  min-width: 2.125em;
  height: 3.375em;
  padding: 0 .625em;
  overflow: hidden;
  border-radius: 2.125em;
}

/********** 组件按钮公用样式 **********/
.audio-controls-button {
  flex: 0 0 2em;
  height: 2em;
  padding: 0;
  overflow: hidden;
  font-size: 100%;
  color: inherit;
  background: transparent;
  border: 0;
  border-radius: 2.25em;
  cursor: pointer;
  transition: background ease-in-out .2s;
}
.audio-controls-button:hover { background: rgba(0,0,0,.06); }
.audio-controls-button::before {
  display: inline-block;
  content: "";
}

/********** 播放按钮 **********/
.audio-controls-button[aria-label="play"]::before {
  transform: translate(30%, .16em);
  border: solid transparent;
  border-width: .55em .65em;
  border-left-color: currentColor;
}

/********** 暂停按钮 **********/
.audio-controls-button[aria-label="pause"]::before {
  width: .375em;
  height: 48%;
  transform: translateY(.16em);
  text-align: center;
  border: solid currentColor;
  border-width: 0 .25em;
}

/********** 声音按钮 **********/
.audio-controls-button[aria-label="volume"] {
  margin-left: 1em;
  margin-right: .25em;
}
.audio-controls-button[aria-label="volume"]::before { display: none; }

/* 声音按钮图标 */
.audio-controls-button-volume-icon {
  display: inline-block;
  padding: .3em .15em;
  margin-left: -.3em;
  transform: translate(-50%, .15em);
  border: .3em solid transparent;
  border-right-color: currentColor;
  box-shadow: inset 1em 0 0;
}
.audio-controls-button-volume-icon > i {
  position: relative;
  left: calc(.3125em * 2);
}
.audio-controls-button-volume-icon > i::before {
  content: "";
  position: absolute;
  width: .7em;
  height: 1.4em;
  transform: translateY(-50%);
  box-sizing: border-box;
  border: .16em solid;
  border-left: 0;
  border-radius: 0 .7em .7em 0;
}
.audio-controls-button-volume-icon > i::after {
  content: "";
  position: absolute;
  width: .3em;
  height: .6em;
  transform: translateY(-50%);
  background: currentColor;
  border-radius: 0 .3em .3em 0;
}

/********** 选项菜单 **********/
.audio-controls-button[aria-label="menu"]::before  {
  width: .3125em;
  height: .3125em;
  transform: translateY(-50%);
  background: currentColor;
  border-radius: 50%;
  box-shadow: 0 .5em 0, 0 -.5em 0;
}

/********** 播放时间文本 **********/
.audio-controls-time {
  font-size: .875em;
  line-height: 1;
  opacity: .85;
}
.audio-controls-time[aria-label="current"] {
  margin-left: .375em;
}
.audio-controls-time[aria-label="remaining"]::before {
  content: "/";
  margin: 0 .25em;
}

/********** 时间轴 **********/
.audio-controls-timeline {
  -webkit-appearance: none;
  flex: 1;
  height: .25em;
  margin-left: 1em;
  font-size: 100%;
  color: inherit;
  background: currentColor;
  border-radius: .25em;
  mix-blend-mode: darken;
}
.audio-controls-timeline::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 0;
  height: 0;
  border: .375em solid;
  border-radius: 50%;
  box-shadow: 50vw 0 0 50vw rgba(255,255,255,.5);
}

DOM基础测试32:长按与范围选取

题目:

image


我的回答:

> 在线 DEMO <

  • 增加了一个矩形范围选框,用以在视觉上直观感受到框选的范围;
  • 兼容移动端
  • 增加了一个按住 box 后“抖动”的效果(感谢袁隆平和他的杂交水稻……)

移动端长按网页会弹出默认菜单,这个菜单会打断长按滑动操作,被这个问题折磨了好久,后来终于找到根因解决了。避坑指南

let boxNums = 20;
new Box(boxNums);  // 调用
class Box {
  constructor(nums = 0) {
    this.nums = isNaN(nums) ? 0 : Math.floor(nums);  // 数量
    this.boxes = [];  // 当前实例的 .box 元素集合
    this.rangeFrame = {  // 范围选框
      el: null,
      x: 0,
      y: 0
    };

    if (nums > 0) {
      this.build();
    }
  }

  // 在 body 元素里创建 .box 盒子
  build() {
    // 使用文档片段,减少性能损耗
    let fragment = document.createDocumentFragment();

    for (let i = 0; i < this.nums; i++) {
      let box = document.createElement('div');

      box.className = 'box';

      // 阻止移动端长按时弹出系统默认菜单
      box.addEventListener('touchstart', e => e.preventDefault());

      this.boxes.push(box);
      fragment.appendChild(box);
    }

    document.body.appendChild(fragment);
    this.bindEvt();
  }

  // 绑定操作事件
  bindEvt() {
    let timer = null;
    let isReady = false;

    // 按下时
    const startHandle = e => {
      e.stopPropagation();

      let elm = e.target;
      
      // “按压”对象为当前实例中创建的 .box 盒子之一
      if (!!~this.boxes.indexOf(e.target)) {
        timer = setTimeout(() => {
          let { clientX, clientY } = e;

          if (e.touches) {  // 移动端
            clientX = e.touches[0].clientX;
            clientY = e.touches[0].clientY;
          }

          isReady = true;
          elm.classList.add('active');
          this.buildRangeFrame(clientX, clientY);
        }, 350);
      } else {  // 点击空白处
        this.boxes.map(box => box.classList.remove('active'));
      }
    };

    // 拖动中
    const moveHandle = e => {
      if (isReady) {
        let { clientX, clientY } = e;

        if (e.touches) {  // 兼容移动端
          clientX = e.touches[0].clientX;
          clientY = e.touches[0].clientY;
        }

        this.updateRangeFrame(clientX, clientY);
      }
    };

    // 松起时,清空定时器(中断高亮操作)
    const endHandle = (e) => {
      e.preventDefault();
      isReady = false;
      clearTimeout(timer);
      this.removeRangeFrame();
    };

    // 按下
    window.addEventListener('touchstart', startHandle);
    window.addEventListener('mousedown', startHandle);

    // 拖动
    window.addEventListener('touchmove', moveHandle);
    window.addEventListener('mousemove', moveHandle);
    
    // 松起
    window.addEventListener('touchend', endHandle);
    window.addEventListener('mouseup', endHandle);
  }

  // 创建范围选框
  buildRangeFrame(posX, posY) {
    let elm = document.createElement('i');

    elm.className = 'range-frame';
    elm.style.left = `${posX}px`;
    elm.style.top = `${posY}px`;
    document.body.appendChild(elm);

    this.rangeFrame.el = elm;
    this.rangeFrame.x = posX;
    this.rangeFrame.y = posY;
  }

  // 更新范围选框
  updateRangeFrame(posX, posY) {
    let { el, x, y } = this.rangeFrame;

    if (posX < x) {  // 向左反方向
      el.style.left = 'auto';
      el.style.right = `${window.innerWidth - x}px`;
    } else {
      el.style.left = `${x}px`;
      el.style.right = 'auto';
    }

    if (posY < y) {  // 向上反方向
      el.style.top = 'auto';
      el.style.bottom = `${window.innerHeight - y}px`;
    } else {
      el.style.top = `${y}px`;
      el.style.bottom = 'auto';
    }

    // 矩形选框尺寸
    el.style.width = `${Math.abs(posX - x)}px`;
    el.style.height = `${Math.abs(posY - y)}px`;

    // 获取矩形区域左上、右下坐标
    // this.computeContains({
    // 	x1: Math.min(posX, x), y1: Math.min(posY, y),
    // 	x2: Math.max(posX, x), y2: Math.max(posY, y)
    // });

    this.computeContains(el.getBoundingClientRect());
  }

  // 移除范围选框
  removeRangeFrame() {
    if (this.rangeFrame.el) {
      document.body.removeChild(this.rangeFrame.el);
      this.rangeFrame.el = null;
    }
  }

  // 计算 box 是否包含在选框区域
  computeContains(area) {
    this.boxes.map(box => {
      let { left, top, width, height } = box.getBoundingClientRect();
      
      // 矩形碰撞检测
      if (
        area.left + area.width > left && left + width > area.left  // 横向
        &&
        area.top + area.height > top && top + height > area.top  // 纵向
      ) {
        box.classList.add('active');
      }
    });
  }
}

分享自己画的矩形碰撞检测示意图:
image

// zxx: 框选有bug,一旦框选经过,框选范围再变小的时候,选中态没有还原。
// wingmeng: 谢谢张老师指点,我题意理解错了……

CSS基础测试4:聊天界面布局

题目:

完成下图所示的布局效果,只要兼容移动端即可:
image


我的答案:

在线Demo


<div class="chatbox">
  <article class="message">
    <div class="user-avatar">
      <img src="https://tva4.sinaimg.cn/crop.0.0.750.750.180/75f2b996jw8f6zkdm7qp7j20ku0kudgr.jpg" alt="提案笙">
    </div>
    <h4 class="message-heading">
      <span>提案笙</span><small>9月30日21:47</small>
    </h4>
    <div class="message-content">什么秘密,我觉得你现在跟我说什么都没有意义。</div>
  </article>
  <article class="message">
    <div class="user-avatar">
      <img src="https://tvax3.sinaimg.cn/crop.135.0.810.810.180/006LO43wly8frjay2sypvj30u00mita5.jpg" alt="淮南王铃">
    </div>
    <h4 class="message-heading">
      <span>淮南王铃</span><small>10月8日10:30</small>
    </h4>
    <div class="message-content">@蝴蝶蓝 优秀</div>
  </article>
  <article class="message">
    <div class="user-avatar">
      <img src="https://tvax4.sinaimg.cn/crop.0.0.512.512.180/6cd7bd09ly8fsnom1e5wtj20e80e8gm7.jpg" alt="蝴蝶蓝">
    </div>
    <h4 class="message-heading">
      <span>蝴蝶蓝</span><small>昨天 22:13</small>
    </h4>
    <div class="message-content">值得一听~~</div>
  </article>
  <article class="message oneself">
    <div class="user-avatar">
      <img src="https://tva1.sinaimg.cn/crop.0.0.750.750.180/006bQeGsjw8f1tgl7z9ncj30ku0kuq44.jpg" alt="Y优秀X">
    </div>
    <h4 class="message-heading">
      <span>Y优秀X</span><small>刚刚</small>
    </h4>
    <div class="message-content">围观戏精现场</div>
  </article>
</div>
html {font-size: 14px;}
body {
  padding: 0;
  margin: 0;			
}

h4 {font-weight: 500;}

.chatbox {
  padding-top: 2rem;
  background: #fff;
}

.message {
  position: relative;
  padding: 0 5.5rem;
  margin-bottom: 2rem;
  font-size: 1.125rem;
}

.user-avatar {
  position: absolute;
  width: 4rem;
  height: 4rem;
  left: calc((5.5rem - 4rem) / 2 - 5px);
  overflow: hidden;
  background: #e6e6e6;  /* 背景色兜底,防止头像“走光” */
  border-radius: 50%;
}

.user-avatar > img {
  display: block;
  max-width: 100%;
  margin: auto;
}

.message-heading {
  margin: 0;
  margin-bottom: .4rem;
  font-size: inherit;
  color: #ababab;
}

.message-heading > span {margin-right: .5em;}

.message-content {
  position: relative;
  display: inline-block;
  min-height: 1em;
  padding: .5em 1em;
  line-height: 1.6;
  background: #f2f4f7;
  border-radius: 10px;
  border-top-left-radius: 0;
}

.message-content::before {
  content: "";
  position: absolute;
  top: 0; left: 0;
  width: calc(5.5rem - 4rem);
  height: calc(5.5rem - 4rem);
  transform: translateX(-100%);

  /* 弧形箭头借鉴了 @guisturdy 的代码 */
  box-shadow: 0.5rem 0 0 #f2f4f7;
  border-radius: 50% 50% 0 0;
}

.message.oneself {direction: rtl;}

.oneself > .user-avatar {
  left: auto;
  right: calc((5.5rem - 4rem) / 2 - 5px);
}

.oneself > .message-heading > span {
  float: right;
  margin-right: 0;
  margin-left: .5em;
}

.oneself > .message-content {
  direction: ltr;
  color: #fff;
  background: #00bdfe;
  border-radius: 10px;
  border-top-right-radius: 0;
}

.oneself > .message-content::before {
  left: auto; right: 0;
  transform: translateX(1rem);
  box-shadow: inset .5rem 0 0 #00bdfe;
}

CSS基础测试10:纯 CSS 实现下拉菜单

题目:

image


我的回答:

> 在线 DEMO <

思路:

  • 利用 :checked 伪类,配合 +(临近选择器) 和 ~(兄弟选择器),来实现子导航展开/折叠的交互效果;
  • 利用 max-heightopacity,配合 transition 实现过渡动画效果;
  • 注意 max-height 应根据实际情况设置合适的值,太大会造成“卡顿”现象,太小会剪裁掉内容;
  • 保留 checkbox 的键盘可访问性,即不使用 display: nonevisibility: hidden 的方式隐藏 checkbox;
  • 使用 visibility 来控制元素的可见性,避免元素视觉不可见时(max-height: 0; opacity: 0),被 Tab 键索引。
<main class="container">
  <nav class="navbar">
    <div class="navbar-group">
      <input class="navbar-group-switch" id="switch1" type="checkbox" checked>
      <label class="navbar-group-title" for="switch1">布局</label>
      <ul class="navbar-group-list">
        <li>
          <a href="#" class="navbar-link">Flex布局</a>
        </li>
        <li>
          <a href="#" class="navbar-link is-active">Grid布局</a>
        </li>
        <li>
          <a href="#" class="navbar-link">Shapes布局</a>
        </li>
        <li>
          <a href="#" class="navbar-link">Columns布局</a>
        </li>
      </ul>
    </div>
    <div class="navbar-group">
      <input class="navbar-group-switch" id="switch2" type="checkbox" checked>
      <label class="navbar-group-title" for="switch2">组件</label>
      <ul class="navbar-group-list">
        <li>
          <a href="#" class="navbar-link">按钮</a>
        </li>
        <li>
          <a href="#" class="navbar-link">输入框</a>
        </li>
        <li>
          <a href="#" class="navbar-link">下拉列表</a>
        </li>
        <li>
          <a href="#" class="navbar-link">单复选框</a>
        </li>
      </ul>
    </div>
  </nav>
</main>
body {
  margin: 0;
  font-size: 14px;
  line-height: 1.42858;
}

.container {
  max-width: 1170px;
  padding: 0 15px;
  margin: 0 auto;
}

/* 导航容器 */
.navbar {
  width: 160px;
  height: 100vh;
  overflow: auto;
  color: #353535;
  box-shadow: inset 1px 0 0 #e0e0e0,
              inset -1px 0 0 #e0e0e0;
}

/* 导航分组标题(点击折叠/展开子导航) */
.navbar-group-title {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px;
  font-weight: 700;
  cursor: pointer;
}
.navbar-group-title::after {
  content: "";
  width: 6px;
  height: 6px;
  transform: rotate(225deg);
  border-style: solid;
  border-width: 2px 0 0 2px;
  transition: transform .3s;
}

/* 子导航列表 */
.navbar-group-list {
  max-height: 0;  /* 用以实现过渡动画 */
  padding: 0;
  margin: 0;
  visibility: hidden;  /* 元素不可见,避免被 tab-index 索引 */
  opacity: 0;  /* 用以实现过渡动画 */
  overflow: hidden;
  transition: all ease-in-out .3s;
  list-style: none;
}

/* 导航链接 */
.navbar-link {
  display: block;
  padding: 0 calc(10px + 1.5em);
  line-height: 40px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  text-decoration: none;
  color: inherit;
}
.navbar-link:hover,
.navbar-link.is-active {
  color: #36b3ee;
  background: rgb(54, 179, 238, .1);
}

/* 控制子导航展开/折叠的复选框 */
.navbar-group-switch {
  /* 剪切式隐藏,保留键盘可访问性 */
  position: absolute;
  clip: rect(0, 0, 0, 0);
}
.navbar-group-switch:focus + .navbar-group-title {
  background: rgb(54, 179, 238, .1);
}
.navbar-group-switch:checked + .navbar-group-title::after {
  transform: rotate(45deg);
}
.navbar-group-switch:checked ~ .navbar-group-list {
  max-height: 500px;  /* 根据实际情况合理设置其值 */
  visibility: visible;
  opacity: 1;
}

JS基础测试37期:数值格式处理/单位换算

题目:

image


我的回答:

第 1 题:

var backCode = '6222081812002934027';

// 方法1
var result = backCode.replace(/(\d{4})/g, '$1 ');
console.log(result);  // "6222 0818 1200 2934 027"

// 方法2
var result = backCode.split(/(\d{4})/).filter(s => !!s).join(' ');
console.log(result);  // "6222 0818 1200 2934 027"

第 2 题:

var numberCode = ‘5702375’;
var result = numberCode.replace(/\d{1,3}(?=(\d{3})+$)/g, '$&,');  // 未考虑小数情况
console.log(result);  // "5,702,375"

第 3 题:

var filesize = 2837475;

function matchUnit(value) {	
  var sizeUnits = ['K', 'M', 'G'];
  var sizeRadix = 1024;

  if (value < sizeRadix) {
    return (value / sizeRadix).toFixed(1) + sizeUnits[0]
  }

  for (var i = sizeUnits.length - 1; i >= 0; i--) {
    var radix = Math.pow(sizeRadix, i + 1);
    if (value >= radix) {
      return (value / radix).toFixed(1) + sizeUnits[i]
    }
  }
}

console.log(matchUnit(filesize));  // 2.7M
console.log(matchUnit(100));  // 0.1K
console.log(matchUnit(10000));  // 9.8K
console.log(matchUnit(100000000));  // 95.4M
console.log(matchUnit(10000000000));  // 9.3G

DOM基础测试34:链接元素的选取与操作

题目:

image


我的回答:

第 1 题:

let a_labels = document.querySelectorAll('a');

第 2 题:

let links = document.querySelectorAll(':link');

第 3 题:

使用 querySelectorAll 获取的元素可直接用 forEach 遍历,或者转换为 Array 类型再遍历

// 3.1
[].slice.call(links).filter(link => {
  if (/^javascript:/i.test(link.href)) {
    link.setAttribute('role', 'button');
    return false;
  }

  return true;
})

// 3.2
.filter(link => {
  // 锚点链接
  if (/^#/.test(link.href)) {   // zxx: 有bug
    return true;
  }

  // 站外链接
  if (!(new RegExp(`^${location.host}`, 'i')).test(link.host)) {
    link.setAttribute('target', '_blank');
    link.setAttribute('rel', 'external nofollow noopener');
    return false;
  }

})

// 3.3
.forEach(link => {
  // 此处接收到的数组只包含以 # 开头的锚点元素(3.2 中已过滤)
  link.setAttribute('rel', 'internal');
});

JS基础测试27:文本校验

题目:

image

我的答案:

var testSpecs = [
    '在LeanCloud上,数据存储是围绕AVObject进行的。',
    '今天出去买菜花了 5000元。',
    '我家的光纤入户宽带有 10Gbps,SSD 一共有 10TB。显示器分辨率宽度是1920px。',
    '今天是 233 ° 的高温。新 MacBook Pro 有 15 % 的 CPU 性能提升。',
    '刚刚买了一部 iPhone ,好开心 !',
    '她竟然对你说「喵」?!?!??!!喵??!!Meow...',
    '你好,我是破折号——一个不苟言笑的符号。',
    '核磁共振成像 (NMRI) 是什么原理都不知道? JFGI!',
    '这件蛋糕只卖 1000 元。',
    '乔布斯那句话是怎么说的?「Stay hungry,stay foolish。」',
    '推荐你阅读《Hackers&Painters:Big Ideas from the Computer Age》,非常的有趣。'
];

charCheck(testSpecs.join(''));

function charCheck(str) {
    // 枚举类型的符号
    var symbols = {
        full: '!()【】『』「」《》“”‘’;:,。?、',
        half: '!-_()[]{}<>"\';:,./?`',
        getRegStr: function(key) {
            var symbols = typeof key === 'string' ? this[key] : (function(that) {
                if (key instanceof Array) {
                    return key.reduce(function(total, cur) {
                        return total += that[cur];
                    }, '');
                }

                return '';
            })(this);

            // 返回符合 regexp 语法的字符串
            return symbols.split('').map(function(s) {
                return '\\' + s;
            }).join('|');
        },
        getRegRule: function(key, usedAs) {
            var strs = this.getRegStr.call(this, 'full');
            var regArr = ['(\\S+)', '([' + strs + '])'];
            var temp = [].concat(regArr);
            temp.reverse();

            if (usedAs === 'rule') {
                return new RegExp(
                    '(:?' + regArr.join('\\s+') + ')|' +
                    '(:?' + temp.join('\\s+') + ')', 'g');
            } else if (usedAs === 'format') {
                return [
                    new RegExp(regArr.join('\\s+'), 'g'),
                    new RegExp(temp.join('\\s+'), 'g')
                ];
            }
        }
    };

    var regExps = [
        {
            rule: /([\u4e00-\u9fa5]+[a-zA-Z]+)|([a-zA-Z]+[\u4e00-\u9fa5]+)/g,
            format: [
                /([\u4e00-\u9fa5]+)([a-zA-Z]+)/g,
                /([a-zA-Z]+)([\u4e00-\u9fa5]+)/g
            ],
            matches: '$1 $2',
            msg: '中英文之间需要增加空格'
        }, {
            rule: /([\u4e00-\u9fa5]+\d+)|(\d+[\u4e00-\u9fa5]+)/g,
            format: [
                /([\u4e00-\u9fa5]+)(\d+)/g,
                /(\d+)([\u4e00-\u9fa5]+)/g
            ],
            matches: '$1 $2',
            msg: '中文与数字之间需要增加空格'
        }, {
            rule: /(\d)([A-Z]+)/g,
            matches: '$1 $2',
            msg: '数字与大写英文单位之间需要增加空格'
        }, {
            rule: /(\d+)\s+(°|%)/g,
            matches: '$1$2',
            msg: '° 或 % 与数字之间不需要空格'
        }, {
            rule: symbols.getRegRule('full', 'rule'),
            format: symbols.getRegRule('full', 'format'),
            matches: '$1$2',
            msg: '全角标点与其他字符之间不加空格'
        }, {
            // rule: new RegExp('(' + symbols.getRegStr(['full', 'half']) + ')\\1+', 'g'),
            rule: new RegExp('(' + symbols.getRegStr('full') + ')\\1+', 'g'),
            matches: '$1',
            msg: '不重复使用中文标点符号'
        }, {
            rule: /(\S)(——)(\S)/g,
            matches: '$1 $2 $3',
            msg: '破折号前后需要增加一个空格'
        }, {
            // 这条必须位于“遇到完整的英文整句、特殊名词,其內容使用半角标点”之前
            rule: new RegExp('\\s*(' + symbols.getRegStr('half') + ')\\s*', 'g'),
            matches: function(s) {
                s = s.replace(/(^\s*)|(\s*$)/g, '');
                return String.fromCharCode(s.charCodeAt() + 65248);
            },
            msg: '使用全角中文标点'
        }, {
            rule: /[\uFF10-\uFF19]/g,
            matches: function(s) {
                // 半角字符与全角字符的 charCode 相差 65248 
                return String.fromCharCode(s.charCodeAt() - 65248);
            },
            msg: '数字使用半角字符'
        }, {
            // 中文的句号“。”不是全角字符,需要特殊处理
            rule: /[《|「](:?\s*[a-zA-Z]+\s*(。|[\uff00-\uffff])*\s*[a-zA-Z]*)+[\.|」|》]/g,
            matches: function(s) {
                return s.replace(/。|[\uff00-\uffff]/g, function($1) {
                    var half = String.fromCharCode($1.charCodeAt() - 65248);

                    if (!!~['&', '-', '+'].indexOf(half)) {  // 需要前后加空格的字符
                        half = ' ' + half + ' ';
                    } else if (!!~[':', ',', ';'].indexOf(half)) {  // 需要后面加空格的字符
                        half += ' ';
                    } else if (half === 'ㄢ') {
                        half = '.';
                    }

                    return half;
                });
            },
            msg: '遇到完整的英文整句、特殊名词,其內容使用半角标点'
        }
    ];

    var result = str;

    regExps.forEach(function(reg, idx) {
        var format = reg.format;
        var matches = reg.matches;
        var tip = str.match(reg.rule);

        if (tip) {
            console.group('%c' + reg.msg + ' (X)', 'color: red');
            console.log(tip.join('\n'));
            console.groupEnd();

            if (!format) {
                result = result.replace(reg.rule, matches);
            } else if (format instanceof Array) {
                format.forEach(function(fmtReg) {
                    result = result.replace(fmtReg, matches);
                });
            } else if (Object.prototype.toString.call(format) === '[object RegExp]') {
                result = result.replace(format, matches);
            }
        }
    });

    if (result === str) {
        console.log('%c当前文本符合规范 (√)', 'color: green');
        return;
    }

    console.group('按规范格式化后的文本:');
    console.log('%c' + result, 'color: green');
    console.groupEnd();
}

image

CSS基础测试13:GitHub Repo 工具条

题目:

image


我的回答:

> 在线 Demo <

<div class="btn-group-container">
  <div class="btn-group">
    <details class="details-menu">
      <summary class="details-menu-btn btn btn-default" role="button" aria-label="watch">
        <svg class="svg-icon">
          <use xlink:href="#icon-watch"></use>
        </svg>
        Unwatch
      </summary>
      <div class="details-menu-modal">
        菜单(略)
      </div>
    </details>
    <a class="btn btn-link" href="#" aria-label="63 users are watching this repository">63</a>
  </div>
  <div class="btn-group">
    <button class="btn btn-default" aria-label="star">
      <svg class="svg-icon">
        <use xlink:href="#icon-star"></use>
      </svg>
      Star
    </button>
    <a class="btn btn-link" href="#" aria-label="445 users starred this repository">445</a>
  </div>
  <div class="btn-group">
    <button class="btn btn-default" aria-label="fork">
      <svg class="svg-icon">
        <use xlink:href="#icon-fork"></use>
      </svg>
      Fork
    </button>
    <a class="btn btn-link" href="#" aria-label="25 users forked this repository">25</a>
  </div>
</div>

<svg style="display:none">
  <!-- svg sprite (代码略) -->
</svg>
body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
}

/** 主容器 **/
.btn-group-container {display: flex;}

/** 按钮组 **/
.btn-group {
  display: flex;
  align-items: center;
}

.btn-group + .btn-group {margin-left: 10px;}
.btn-group .btn {border-radius: 0;}
.btn-group > .btn:not(:first-child),
.btn-group > .details-menu:not(:first-child) {margin-left: -1px;}

.btn-group > .btn:first-child,
.btn-group > .details-menu:first-child > .btn {
  border-top-left-radius: .25em;
  border-bottom-left-radius: .25em;
}

.btn-group > .btn:last-child,
.btn-group > .details-menu:last-child > .btn {
  border-top-right-radius: .25em;
  border-bottom-right-radius: .25em;
}

/** 按钮 **/
.btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  padding: 3px 9px;
  font-size: 12px;
  font-weight: 600;
  line-height: 20px;
  white-space: nowrap;
  vertical-align: middle;
  cursor: pointer;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
  font-family: inherit;
  background-repeat: repeat-x;
  background-position: -1px -1px;
  background-size: 110% 110%;
  border: 1px solid #ccced0;
  border-radius: .25em;
  text-decoration: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
}

.btn:hover {z-index: 1;}

.btn > .svg-icon {
  width: 20px;
  height: 20px;
  margin-right: 2px;
  fill: currentColor;
  speak: none;
}

/** 按钮主题定义 **/
.btn-default {
  color: #24292e;
  background-color: #eff3f6;
  background-image: linear-gradient(-180deg, #fafbfc, #eff3f6 90%);
}

.btn-default:hover {
  background-color: #e6ebf1;
  background-image: linear-gradient(-180deg, #f0f3f6, #e6ebf1 90%);
  background-position: -.5em;
  border-color: #a5a8ac;
}

.btn-link {
  color: #24292e;
  background-color: #fff;
}

.btn-link:hover {color: #0366d6;}

/** 按钮下拉菜单 **/
.details-menu {
  position: relative;
  display: block;
  
}

.details-menu-btn::-webkit-details-marker {display:none;}
.details-menu-btn::after {
  content: "";
  display: inline-block;
  width: 0;
  height: 0;
  margin-left: 3px;
  transform: translateY(25%);
  border: 4px solid transparent;
  border-top-color: currentColor;
}

.details-menu-modal {
  position: absolute;
}

JS基础测试40:字符串格式书写方式转换

题目:

image


我的回答:

//zxx: 1,3测试没过

第 1 题:

function toCamelCase(str) {
  return str.replace(/-([a-z])/g, ($1, $2) => $2.toUpperCase());
}

第 2 题:

function toDashJoin(str) {
  return str.replace(/[A-Z]/g, '-$&').toLowerCase();  // $&:表示匹配到的内容
}

第 3 题:

function toCapitalize(str) {
  return str.split(/\s+/).map(s => s.substr(0, 1).toUpperCase() + s.substr(1)).join(' ');
}

第 4 题:

function toBetterUrl(str) {
  return str.split(/\s+/).map(s => s.toLowerCase()).join('-');
}

CSS基础测试16:两栏垂直居中布局

题目:

补全代码,完成两栏垂直居中布局,地址:http://quiz.xiliz.com/css-quiz16.html
每个方法2积分,每个方法都注意视觉还原效果,如果效果不佳会酌情扣分。

我的回答:

/* 方法1(IE8+) */
.quiz {
  display: table;
}
.quiz-h {
  padding: 0 10px;
  margin: 0;
  vertical-align: middle;
}
.quiz-p {
  margin: 0 5px;
}

/* 方法2(IE10+) */
.quiz {
  align-items: center;
}
.quiz-h {
  padding: 0 10px;
  margin: auto;
}
.quiz-p {
  flex: auto;
  margin: 0 5px;
}

/* 方法3(IE8+) */
.quiz {
  white-space: nowrap;
}
.quiz-h {
  margin: 0;
  padding: 0 10px;
  vertical-align: middle;
}
.quiz-p {
  margin: 0;
  margin-right: 90px;  /* .quiz-h 的宽度(79.2px)+ 间距(10px) */
  vertical-align: middle;
  white-space: normal;
}

/* 方法4(IE8+) */
.quiz {
  position: relative;
}
.quiz-h {
  top: 0;
  bottom: 0;
  height: 1.5em;
  margin: auto 10px;
}
.quiz-p {
  margin: 0 5px 0 80px;
}

JS基础测试39:HEX 颜色处理和伪随机色

题目:

  1. 补全hex颜色值,如 #0 或者 0 都是合法的,1位,2位,3位,6位字符串都是合法的,例如:#A -> #aaaaaa,或者 a -> #aaaaaa;
  2. 获取一个随机的hex颜色值;
  3. 获取一个介于a,b之间的随机颜色。

我的回答:

第 1 题:

function colorPad(hex) {
  const reg = /^#?([0-9a-f]{1,3}|[0-9a-f]{6})$/i;
  let result = '#' + '0'.repeat(6);

  if (reg.test(hex)) {
    result = hex.replace(reg, (_, $1) => {
      $1 = $1.toLowerCase();

      switch($1.length) {
        case 6:
          return '#' + $1;
        case 3:
          return '#' + $1.split('').map(s => s.repeat(2)).join('');
        default:
          return '#' + $1.repeat(6 / $1.length);
      }
    });
  }

  return result;
}

第 2 题:

// http://randomcolour.com/
function randomHex() {
  const range = Math.pow(16, 6) - 1;
  let color = Math.floor(Math.random() * range).toString(16);
  color = '#' + ('0'.repeat(6) + color).slice(-6);

  return color;
}

第 3 题:

// zxx: 我的测试用例测试下来有问题
function colorRange(a, b) {
  let start = parseInt(colorPad(a).substr(1), 16);
  let end = parseInt(colorPad(b).substr(1), 16);

  if (start > end) {
    [start, end] = [end, start];
  }

  return '#' + (Math.round(Math.random() * (end - start)) + start).toString(16);
}
测试用例

https://codepen.io/wingmeng/pen/MWWmbLO

// colorPad 测试
const testCases = [
  { it: '0', expect: '#000000' },
  { it: '#0', expect: '#000000' },
  { it: 'a', expect: '#aaaaaa' },
  { it: '#A', expect: '#aaaaaa' },
  { it: 'AF', expect: '#afafaf' },
  { it: '#fc0', expect: '#ffcc00' },
  { it: '#936c', expect: '#000000' },
  { it: '#936ce', expect: '#000000' },
  { it: '#936cef', expect: '#936cef' },
  { it: '#EDFGHI', expect: '#000000' }
];

testCases.map(c => {
  const result = colorPad(c.it);
  const isPassed = result === c.expect;

  console.group(`colorPad("${c.it}") -> "${c.expect}"`);
  isPassed ?
    console.log('%c√ Pass', 'color: green') :
    console.log('%c× Failed! the actual result is: ' + result, 'color: red');
  console.groupEnd();
});

// 随机 hex 颜色测试
const testNumbers = 10;
const hexReg = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;

[...new Array(testNumbers)].map(() => {
  const result = randomHex();

  console.group(`randomHex() -> "${result}"`);
  hexReg.test(result) ?
    console.log('%c√ Pass', 'color: green') :
    console.log('%c× Failed', 'color: red');
  console.groupEnd();
});

// 颜色范围测试
[...new Array(testNumbers)].map(() => {
  const color1 = randomHex();
  const color2 = randomHex();
  const result = colorRange(color1, color2);

  console.group(`colorRange("${color1}", "${color2}") -> "${result}"`);
  hexReg.test(result) ?
    console.log('%c√ Pass', 'color: green') :
    console.log('%c× Failed', 'color: red');
  console.groupEnd();
});

JS基础测试32:cookie 及 localStorage 操作

题目:

image


我的回答:

  • 第 1 题:document.cookie

  • 第 2 题:document.cookie = 'userid=1';

  • 第 3 题:

var expires = 24 * 36e5;  // 过期时间:1 天
document.cookie = 'userid=1; expires=' + (new Date((+new Date(time)) + expires)).toUTCString();
  • 第 4 题:
getCookie('_csrfToken');
function getCookie(name) {  
  var arr = document.cookie.split(/;\s/);

  for (var i = 0; i < arr.length; i++) {
    var params = arr[i].split('=');

    if (name === params[0]) {
      return params[1];
    }
  }

  return '';
}
  • 第 5 题:
document.cookie = 'ywkey=; expires=' + new Date(Date.now() - 1e10).toUTCString();
  • 第 6 题:localStorage.setItem('userid', 1);

  • 第 7 题:

var expires = 24 * 36e5 * 7;  // 过期时间:7 天

// 存储时加个时间戳
localStorage.setItem('userid', JSON.stringify({
  data: 1,
  stamp: Date.now() + expires
}));

// 取值时进行判断
var store_userid = JSON.parse(localStorage.getItem('userid'));
var uidStamp = store_userid.stamp;
var userid;

if (uidStamp <= Date.now()) {  // 过期
  localStorage.removeItem('userid');
} else {
  userid = store_userid.data;
}

CSS基础测试11:列表式卡片布局

题目:

image


我的回答:

> 在线 Demo <

//zxx: Firefox是有打点效果的

image

.list {
  width: 300px;
  box-sizing: border-box;
  line-height: 1.25;
  font-size: 13px;
  background: #fff8dc;
  border: 1px solid #e6dfc6;
  border-radius: 2px;
}

.list-title,
.list-item {padding: 12px 15px;}

.list-title {
  font-weight: 700;
  color: #737063;
  background: #f7f1d5;
}

.list-item {
  display: flex;
  margin: 0;
  border-top: 1px solid #ede7cd;
}

.list-item > a {
  /* 仅 Webkit 内核浏览器支持多行溢出打点效果,其他浏览器只截断 */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;

  /* 设置最小和最大高度 */
  min-height: calc(13px * 1.25 * 1.25);
  max-height: calc(13px * 1.25 * 2);  /* 字体尺寸×行高×行数(2行) */

  overflow: hidden;
  text-overflow: ellipsis;
  text-decoration: none;
  color: #0077cc;
}

.list-item > .iconfont {
  margin-right: .5em;
  font-size: 16px;
}
.list-item > .icon-dialog {color: #46a2d9;}
.list-item > .icon-stack-overflow {color: #52575c;}
<!-- 使用了第三方 iconfont 资源 -->
<dl class="list">
  <dt class="list-title">标题</dt>
  <dd class="list-item">
    <i class="iconfont icon-dialog"></i>
    <a href="##">恭喜我们29个最老的测试版网站-他们现在不再是测试版了!</a>
  </dd>
  <dd class="list-item">
    <i class="iconfont icon-dialog"></i>
    <a href="##">《独角兽动物园》35;7:Nicolas访谈</a>
  </dd>
  <dd class="list-item">
    <i class="iconfont icon-stack-overflow"></i>
    <a href="##">减少需要重新审查的封闭问题数量的建议</a>
  </dd>
  <dd class="list-item">
    <i class="iconfont icon-stack-overflow"></i>
    <a href="##">实验:在接下来的30天内(直到2019-09-07),以3票为基准关闭和重新打开提出的问题</a>
  </dd>
</dl>

CSS基础测试7:紧贴底部的页脚

题目:

image


我的回答:

> 在线 Demo <

body {margin: 0;}
main, content, footer {display: block;}

.container {
  display: flex;
  flex-flow: column;
  min-height: 100vh;  /* 不设置固定高度 */
}

.content {flex: 1;}

.footer {
  color: #fff;
  background: #555;
}

DOM基础测试40:列表点击、方向键操作

题目:

image

我的回答:

> 在线 Demo <

const viewList = {
  init() {
    this.input = document.getElementById('input');
    this.list = document.getElementById('list');
    this.listItems = this.list.querySelectorAll('li');
    this.selectedIdx = -1;
    this.clickHandle();
    this.keyingHandle();
  },
  clickHandle() {
    this.listItems.forEach((li, index) => {
      li.addEventListener('click', () => this.doSelect(index));
    });
  },
  keyingHandle() {
    document.addEventListener('keydown', ({ keyCode }) => {
      const maxIndex = this.listItems.length - 1;
      let index = this.selectedIdx;

      if (keyCode === 38) {  // ↑
        index--
      } else if (keyCode === 40) {  // ↓
        index++
      }

      index = index < 0 ? maxIndex : index;
      index = index > maxIndex ? 0 : index;

      this.doSelect(index);
    });
  },
  doSelect(index) {
    const curLi = this.listItems[index];
    const selectedIdx = this.selectedIdx < 0 ? 0 : this.selectedIdx;

    this.listItems[selectedIdx].classList.remove('selected');
    curLi.classList.add('selected');
    this.input.value = curLi.innerText;
    this.selectedIdx = index;
  }
};

viewList.init();

CSS基础测试15:网格导航布局

题目:

image

我的回答:

> 在线 Demo <

html {font-size: calc(16 / 375 * 100vw);}

.grid-nav {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  overflow: hidden;  /* 隐藏溢出的 box-shadow */
  background: #524940;
}

.grid-nav-item {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  padding: calc((100% - 1rem * 1.15 - 2rem - .5rem) / 2) 0;  /* 正方形容器 */
  line-height: 1.15;
  color: #f7f1ee;
  box-shadow: 16px 0 0 -15px rgba(255, 255, 255, .5),  /* 单侧投影 */
              0 16px 0 -15px rgba(255, 255, 255, .5);
  text-decoration: none;
}

.grid-nav-item > .iconfont {
  margin-bottom: .5rem;
  font-size: 2rem;
  line-height: 1;
}
<nav class="grid-nav">
  <a href="#" class="grid-nav-item">
    <i class="iconfont icon-clock" aria-hidden="true"></i>
    <span>商品秒杀</span>
  </a>
  <a href="#" class="grid-nav-item">
    <i class="iconfont icon-business" aria-hidden="true"></i>
    <span>企业团购</span>
  </a>
  <a href="#" class="grid-nav-item">
    <i class="iconfont icon-private" aria-hidden="true"></i>
    <span>私码通道</span>
  </a>
  <a href="#" class="grid-nav-item">
    <i class="iconfont icon-sim-card" aria-hidden="true"></i>
    <span>铁粉卡</span>
  </a>
  <a href="#" class="grid-nav-item">
    <i class="iconfont icon-exchange" aria-hidden="true"></i>
    <span>以旧换新</span>
  </a>
  <a href="#" class="grid-nav-item">
    <i class="iconfont icon-phone-bill" aria-hidden="true"></i>
    <span>话费充值</span>
  </a>
</nav>

DOM基础测试37:<dialog> 元素

题目:

image


我的回答:

第 1 题:

const dialog = document.createElement('dialog');
document.body.appendChild(dialog);

第 2 题:

// 方法1:
dialog.open = true;  // 或其他为“真值”的基本类型,如 1,"abc" 等

// 方法2:
dialog.show();

// 方法3:
dialog.showModal();

第 3 题:

const btn = document.createElement('button');

btn.innerText = 'close';
btn.addEventListener('click', () => {
  // 方法1:
  dialog.open = false;  // 或其他“非真”的基本类型,如 0、null 等

  // 方法2:
  dialog.close();
});
dialog.appendChild(btn);

第 4 题:

// 用 showModal 方法即可让打开的 dialog 自带遮罩,在 CSS 里可通过 ::backdrop 设置遮罩层样式
dialog.showModal();

第 5 题:

思路:如果是 showModal() 方法打开的 dialog,则其覆盖层级默认是置顶的;而通过 show() 方法或 open 属性打开的 dialog,其覆盖层级遵循“后来居上”原则,所以需要手动调整其 z-index 值来使其覆盖层级置顶。

(function(dialogElm) {
  if (!dialogElm) return;

  const proto = dialogElm.prototype;
  const oldShow = proto.show;
  const dialogBaseLevel = 100;  // 对话框弹层的基准层级(根据项目zIndex规范合理设置)
  const getMaxZIndex = () => {
    const dialogs = document.querySelectorAll('dialog[open]');
    const maxZIndex = Math.max.apply(null, [...dialogs].map(it =>
      it.style.zIndex || Number(window.getComputedStyle(it).zIndex) || dialogBaseLevel
    ));

    return maxZIndex;
  };
  const setMaxZIndex = el => el.style.zIndex = getMaxZIndex() + 1;

  // 重写 show 方法
  proto.show = function() {
    setMaxZIndex(this);
    oldShow.call(this);
  };

  // "劫持" open 属性
  Object.defineProperty(proto, 'open', {
    set: function(value) {
      const isOpen = Boolean(isNaN(value) ? value : Number(value));
      this[isOpen ? 'show' : 'close']();
    }
  });
})(window.HTMLDialogElement);

测试 Demo

DOM基础测试27:返回顶部

题目:

image


我的答案:

第 1 题和第 2 题,两大问题,一个对策。已封装成组件,只需为构造函数传入需要“返回顶部”功能的容器元素即可,默认为 body 元素。

在线演示

.back-top {
  position: fixed;
  margin-top: -15px;
  margin-left: -15px;
  transform: translate(-100%, -100%);
  white-space: nowrap;
}
/**
 * 返回顶部
 * @descr: 给定一个容器,当滚动高度超出1倍的容器高度,显示返回顶部,否则隐藏
 * @param {object HTMLElement} [elm] - HTML元素,可选,默认为 body 元素
 */
function ScrollBackTop(elm) {
  var that = this;
  var timer = null;

  this.elm = elm || document;
  this.elmBox = elm || document.documentElement;
  this.render();

  // 监听容器 scroll 事件
  this.elm.addEventListener('scroll', function() {
    clearTimeout(timer);
    timer = setTimeout(function() {
      that.scrolling();
    }, 20);
  }, false);

  // 监听视窗尺寸改变
  window.addEventListener('resize', function() {
    clearTimeout(timer);
    timer = setTimeout(function() {
      that.scrolling();
    }, 20);
  }, false);
}

// 生成按钮
ScrollBackTop.prototype.render = function() {
  var that = this;
  var btn = document.createElement('a');
  var text = document.createTextNode('返回顶部↑');

  btn.className = 'back-top';
  btn.href = '#';
  btn.hidden = true;

  // 监听点击
  btn.addEventListener('click', function(event) {
    if (that.elm === document) {
      window.scrollTo(0, 0);  // 兼容移动端
    } else {
      that.elmBox.scrollTop = 0;
    }

    event.preventDefault();
    return false;
  }, false);

  btn.appendChild(text);
  (this.elm === document ? document.body : this.elm).appendChild(btn);

  this.btn = btn;
}

ScrollBackTop.prototype.scrolling = function() {
  var elmBox = this.elmBox;
  var scroll_y = this.elm === document ? window.pageYOffset : elmBox.scrollTop;
  var offset_x = elmBox.offsetLeft;
  var offset_y = elmBox.offsetTop;
  var x = elmBox.clientWidth;
  var y = elmBox.clientHeight;

  if (scroll_y >= y) {
    this.btn.hidden = false;

    // 按钮的显示位置
    this.btn.style.top = offset_y + y + 'px';
    this.btn.style.left = offset_x + x + 'px';
  } else {
    this.btn.hidden = true;
  }
}

new ScrollBackTop();  // 整个文档返回顶部的场景
new ScrollBackTop(document.getElementById('divBox'));  // 某个 div 容器的场景

JS基础测试28:数组项位移、互换位置

题目:

image


我的答案:

既然题目中用了 let ,那我就不客气了,ES6 直接用起。:tada:
主要原理是数组的 splice 操作,然后是处理各种边界情况。
封装了一下,使用链式调用,使方法看起来更语义化。


let arr = [0, 1, 2, 3, 4, 5];

// 返回对应索引的数组项
Array.prototype.index = function(index) {
    if (!(typeof index !== 'boolean' && Number.isInteger(Number(index)))) {
        throw (`\`${index}\` is not a integer.`);
    }

    let arr = [...this];  // 使用原数组的副本,以免后续操作影响原数组

    return {
        arr,
        index,
        value: arr[index]
    };
}

// 移动数组项
Object.prototype.moveTo = function(target) {
    let { arr, index, value } = this;

    const _moveTo = (targetIdx) => {
        if (!Number.isInteger(targetIdx)) {
            throw (`\`${targetIdx}\` is not a integer.`);
        }

        if (targetIdx > arr.length - 1) {
            targetIdx = arr.length - 1;
        } else if (targetIdx < 0) {
            targetIdx = 0;
        }
        
        arr.splice(index, 1);  // 先删除原来的
        arr.splice(targetIdx, 0, value);  // 在目标索引补上原来的
    };

    if (!arr.includes(value)) {  // 不存在
        throw (`The current index(\`${index}\`) of item is not in the array([${arr}])`);
    }
    
    // 各种条件判断
    if (target === 'start' || target <= 0) {  // 移到开头
        _moveTo(0)
    } else if (target === 'end' || target >= arr.length - 1) {  // 移到结尾
        _moveTo(arr.length);
    } else if (target === 'forward') {  // 向前
        _moveTo(index - 1);
    } else if (target === 'backward') {  // 向后
        _moveTo(index + 1);
    } else {
        _moveTo(target);
    }

    return arr;
}

// 交换数组项位置
Object.prototype.swapWith = function(idx) {
    let { arr, index, value } = this;

    if (!arr.includes(value)) {  // 不存在
        throw (`The current index(\`${idx}\`) of item is not in the array([${arr}])`);
    }

    if (!(typeof idx !== 'boolean' && Number.isInteger(Number(idx)))) {
        throw (`\`${idx}\` is not a integer.`);
    }

    if (idx > arr.length - 1 || idx < 0 ) {
        throw (`The index(\`${idx}\`) of item is not in the array([${arr}])`);
    }

    arr[index] = arr.splice(idx, 1, value)[0];

    return arr;
}

console.log(arr.index(3))

console.group('任意数组项向前移一位');
    console.log(arr.index(0).moveTo('forward'));  // [0, 1, 2, 3, 4, 5]
    console.log(arr.index(3).moveTo('forward'));  // [0, 1, 3, 2, 4, 5]
    console.log(arr.index(5).moveTo('forward'));  // [0, 1, 2, 3, 5, 4]
console.groupEnd();

console.group('任意数组项向后移一位');
    console.log(arr.index(0).moveTo('backward'));  // [1, 0, 2, 3, 4, 5]
    console.log(arr.index(3).moveTo('backward'));  // [0, 1, 2, 4, 3, 5]
    console.log(arr.index(5).moveTo('backward'));  // [0, 1, 2, 3, 4, 5]
console.groupEnd();

console.group('将对应数组项移动到最前面');
    console.log(arr.index(2).moveTo(0));  // [2, 0, 1, 3, 4, 5]
    console.log(arr.index(1).moveTo(-10));  // [1, 0, 2, 3, 4, 5]
    console.log(arr.index(5).moveTo('start'));  // [5, 0, 1, 2, 3, 4]
console.groupEnd();

console.group('将对应数组项移动到最后面');
    console.log(arr.index(2).moveTo(arr.length - 1));  // [0, 1, 3, 4, 5, 2]
    console.log(arr.index(1).moveTo(100));  // [0, 2, 3, 4, 5, 1]
    console.log(arr.index(3).moveTo('end'));  // [0, 1, 2, 4, 5, 3]
console.groupEnd();

console.group('任意两个数组项交换位置');
    console.log(arr.index(0).swapWith(5));  // [5, 1, 2, 3, 4, 0]
    console.log(arr.index(3).swapWith(1));  // [0, 3, 2, 1, 4, 5]
    console.log(arr.index(4).swapWith(5));  // [0, 1, 2, 3, 5, 4]
console.groupEnd();

console.group('边界情况验证');
    // 目标索引值超出数组范围,按数组边界处理
    console.log(arr.index(2).moveTo(-1));  // [2, 0, 1, 3, 4, 5]
    console.log(arr.index(2).moveTo(100));  // [0, 1, 3, 4, 5, 2]

    // 字符串数字按数字处理
    console.log(arr.index('2').moveTo('123'));  // [0, 1, 3, 4, 5, 2]
    console.log(arr.index('3').swapWith('4'));  // [0, 1, 2, 4, 3, 5]

    // 非法索引值,报错
    // Error: `abc` is not a integer.
    console.log(arr.index('abc').moveTo(4));
    console.log(arr.index(4).moveTo('abc'));
    console.log(arr.index(4).swapWith('abc'));

    // 索引元素不存在,报错
    // Error: The index(`100`) of item is not in the array([0,1,2,3,4,5])
    // Error: The index(`-2`) of item is not in the array([0,1,2,3,4,5])
    console.log(arr.index(100).moveTo(5));
    console.log(arr.index(5).swapWith(100));
    console.log(arr.index(5).swapWith(-2));

    // 非数组调用,报错(JS内部报语法错误)
    // Error: "123".index is not a function
    console.log('123'.index(2).swapWith(5));
console.groupEnd();

CSS基础测试1:卡片式布局

题目:

image


我的答案:

dl {
    max-width: 400px;
    padding: 5px 15px;
    line-height: 2;
    box-sizing: border-box;
    border: 1px solid #ccc;
    box-shadow: 1px 1px 0 rgba(0,0,0,.15);
}

dt {display: inline;}

/* 灵感来自《CSS揭秘》P116 */
dd + dt::before {
    content: '\A';
    white-space: pre;
}

dd {
    float: right;
    margin: 0;
}

demo min

DOM基础测试29:closest 和 closestAll 方法的实现

题目:

image


我的答案:

我的回答:用 jQuery 的 closest() 方法。开个玩笑 😆

先说下思路:根据传入的参数,使用 querySelectorAll() 方法找出所有匹配的选择器,再匹配出包含当前元素的 nodeList 列表。

第二题

第一题依赖这个方法,所以它先上

Element.prototype.closestAll = function(targetEl) {
  var el = this;
  var nodelist;

  // 参数校验
  if (typeof targetEl !== 'string' || !targetEl.trim()) {
    throw Error('\' + targetEl + \' is not a valid selector');
  }

  nodelist = document.querySelectorAll(targetEl);

  // 使用 ES5 的 filter 过滤出包含 el 的元素
  return [].slice.call(nodelist)
    .filter(function(node) {
      return node.contains(el)
    })
    .reverse();  // 反转数组,最近的排前面,依次从近到远
}

第一题

获取 closestAll() 方法返回的第一项即可

// closest polyfill
window.Element && 'closest' in Element.prototype || +function() {
  Element.prototype.closest = function(targetEl) {
    var result = this.closestAll(targetEl);
    return result.length === 0 ? null : result[0];
  }
}();

JS基础测试33:自定义 toFixed 方法

题目:

image


我的回答:

Chrome Firefox IE11
0.6.toFixed(0) 1 1 1
1.6.toFixed(0) 2 2 2
0.035.toFixed(2) 0.04 0.04 0.04
0.045.toFixed(2) 0.04 0.04 0.05
Number.prototype.toFixed = myToFixed;
String.prototype.toFixed = myToFixed;

function myToFixed(digits = 0) {
  let num = String(this);

  // 小数位不足时后置补零
  const postfixZero = s => {
    let len = digits;

    if (s.indexOf('.') > 0) {
      len = digits - s.split('.')[1].length;
    } else if (len > 0) {
      s += '.';  // 整数位后补小数点
    }

    return s + '0'.repeat(len);
  };

  digits = parseInt(digits, 10);

  if (isNaN(digits)) {  // 非数值
    throw new Error('digits argument must be a number(or string number)');
  }

  if (digits < 0) {  // 负数
    throw new Error('digits argument must be greater than or equal to 0');
  }

  if (!isFinite(num)) {  // +-Infinity
    return num;
  }

  if (num.indexOf('.') > 0) {
    let times = Math.pow(10, digits);  // 根据小数位长度获取升幂倍数
    num = Math.round(num * times) / times + '';  // 四舍五入,降幂
  }

  return postfixZero(num);
}

测试用例:

console.group('普通场景测试');
  console.log(0.6.toFixed(0));  // "1"
  console.log(1.6.toFixed(0));  // "2"
  console.log(0.035.toFixed(2));  // "0.04"
  console.log(0.045.toFixed(2));  // "0.05"
console.groupEnd();    
  
console.group('进阶场景测试');
  console.log(Math.PI.toFixed(10));  // "3.1415926536"
  console.log(0.9.toFixed());  // "1"
  console.log(Number(5).toFixed(2));  // "5.00"
  console.log(3..toFixed(3));  // "3.000"
  console.log(.5.toFixed('3'));  // "0.500"
  console.log(1.2345.toFixed(2.6));  // "1.23"
console.groupEnd();
  
console.group('参数异常测试');
  console.log(Infinity.toFixed(5));  // "Infinity"
  console.log(0.5.toFixed(-2));  // Error: digits argument must be greater than or equal to 0
  console.log(0.5.toFixed(null));  // Error: digits argument must be a number(or string number)
console.groupEnd();
//zxz:  滴~满分~

CSS基础测试14:steps - 步骤条

题目:

image


我的回答:

  • 方案一:flex + clip-path

> Demo <

<ol class="steps">
  <li>1-规则说明</li>
  <li>2-参与活动</li>
  <li class="is-current">3-参与抽奖</li>
  <li>4-奖品发放</li>
  <li>5-查看结果</li>
</ol>
.steps {
  display: flex;
  padding: 0; margin: 0;
  list-style: none;
}

.steps > li {
  display: flex;
  align-items: center;
  height: 40px;
  padding: 0 2em;
  margin-left: -13px;  /* 15px - 13px = 2px(间隙) */
  font-size: 14px;
  color: #009fe9;
  clip-path: polygon(
    0 0, calc(100% - 15px) 0,
    100% 50%, 100% 50%,
    calc(100% - 15px) 100%, 0 100%,
    15px 50%
  );
  background: #edf9ff;
}

.steps > li:first-child {
  margin-left: 0;
  clip-path: polygon(
    0 0, calc(100% - 15px) 0,
    100% 50%, 100% 50%,
    calc(100% - 15px) 100%, 0 100%
  );
}

.steps > .is-current {
  color: #fff;
  background: #009fe9;
}

.steps > .is-current ~ li {
  color: #8c8c8c;
  background: #ebedf0;
}
  • 方案二:table + 伪元素(兼容IE9+)

> Demo <

<!-- HTML 结构同方案一 -->
.steps {
  display: table;
  border-collapse: separate;
  border-spacing: 30px 0;
  padding: 0; margin: 0;
  list-style: none;
}

.steps > li {
  position: relative;
  display: table-cell;
  height: 40px;
  padding: 0 1em 0 .5em;
  vertical-align: middle;
  font-size: 14px;
  color: #009fe9;
  background: #edf9ff;
}

.steps > li::before,
.steps > li::after {
  content: "";
  position: absolute;
  top: 0;
  border: solid transparent;
  border-width: 20px 14px;  /* (15 - 14) × 2 = 2px(间隙) */
}

.steps > li::before {
  left: 0;
  transform: translateX(-100%);
  border-color: #edf9ff;
  border-left-color: transparent;
}

.steps > li::after {
  right: 0;
  transform: translateX(100%);
  border-left-color: #edf9ff;
}

.steps > li:first-child::before {border-left-color: #edf9ff;}

.steps > .is-current {
  color: #fff;
  background: #009fe9;
}

.steps > .is-current::before {
  border-color: #009fe9;
  border-left-color: transparent;
}

.steps > .is-current:first-child:before {border-left-color: #009fe9;}

.steps > .is-current::after {border-left-color: #009fe9;}

.steps > .is-current ~ li {
  color: #8c8c8c;
  background: #ebedf0;
}

.steps > .is-current ~ li::before {
  border-color: #ebedf0;
  border-left-color: transparent;
}

.steps > .is-current ~ li::after {border-left-color: #ebedf0;}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.