加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
文件
克隆/下载
index.html 11.76 KB
一键复制 编辑 原始数据 按行查看 历史
张鑫旭 提交于 2022-09-20 18:43 . update index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>划词评论基本交互功能演示</title>
<style>
body {
max-width: 860px;
margin-inline: auto;
}
h1 {
font-size: 200%;
margin: 1em 0 .75em;
}
h3 {
font-size: 125%;
margin: 1em 0 .75em;
}
.container {
display: flex;
border: 1px solid #ccc;
border-top: solid #999;
min-height: calc(100vh - 400px);
}
.comment {
width: 28%;
border-left: 1px solid #ccc;
margin-left: 1em;
}
.article {
flex: 1;
padding: 1rem;
}
.comment:not(.active) {
display: none;
}
.comment-header {
display: flex;
padding: .5rem 1rem;
background-color: #f0f0f0;
}
.comment-form {
padding: 1rem;
}
.comment-header button {
margin-left: auto;
}
.comment-filed {
box-sizing: border-box;
width: 100%; height: calc(4.5em + 6px);
padding: 6px 8px;
line-height: 1.5;
}
.content {
line-height: 1.75;
margin-block: 1em;
}
.content .word {
border-bottom: 1px solid #ffd666;
padding-bottom: 1px;
}
.word.active {
background-color: #fffbe6;
}
[contenteditable] {
border: 1px solid #ddd;
padding: .5em;
}
/* 跟随选区的悬浮按钮 */
.popover-button {
position: absolute;
z-index: 9;
display: none;
}
textarea, button {
font-size: .875rem;
}
</style>
</head>
<body id="body">
<h1>划词评论基本交互功能演示</h1>
<p>主要演示划词部分,这是实现难点,评论功能因为出入较大,这里简单走个过场,具体细节大家自己实现。</p>
<p>本演示为原生语言,如果大家使用Vue或React开发,稍微换换格式就可以了,兼容的,通用的哈~</p>
<p>由于gitee page只支持静态页面,因此,相关请求都是死数据,或者使用console示意一下。</p>
<h3>关于测试内容</h3>
<p>其中第一段为非编辑态,演示划词交互能力;第二段是编辑态,演示如何实时保存编辑后的选区起止位置和内容。</p>
<br>
<div id="container" class="container">
<div id="article" class="article">
<div class="content" data-pid="p0">《CSS新世界》上线1个月多一点,编辑跟我说居然已经重印了,说实话,有些意外,毕竟首印有 4000 册呢,感谢大家的支持,果然,用心的作品总是会得到大家的认可。</div>
<div class="content" contenteditable="true" data-pid="p1">大家尽管选便宜的时候买,京东每隔一段时间都会做活动,满100减50这种,趁这个时候买就好了,算下来,5折6折的样子,很划算,就这个时候买,不要觉得买的太便宜心里有负担,跟大家讲,哪怕京东买10块钱一本,也不影响我的版税收益的。</div>
</div>
<div id="comment" class="comment">
<div class="comment-header">评论<button id="shut">关闭</button></div>
<ul id="result"></ul>
<form class="comment-form" id="form">
<textarea id="textarea" class="comment-filed" name="content" placeholder="评论内容" required></textarea>
<p>
<button>提交</button>
</p>
</form>
</div>
</div>
<!-- 悬浮评论按钮 -->
<button id="popover" class="popover-button">+</button>
</body>
<!-- 实现逻辑代码就这里了 -->
<script type="module">
import { showSelectionPopover, hideSelectionPopover, doRangeWrapHighLight, getContentAndIndex, getNodeAndOffset, getContentAndIndexList } from './src/utils.js';
// 实际开发的时候
// 内容的请求和评论数据的请求建议分开
// 这里就偷懒了
// 内容直接在页面显示了
// 不影响功能的演示
// 全局的选择器
const selector = '.content';
// 全部的数据
const data = {
data: []
};
// 高亮方法
// 此方法不支持重复执行
const doHighLight = function () {
data.data.forEach(obj => {
const pid = obj.pid;
const eleInput = document.querySelector(selector + '[data-pid="'+ pid +'"]');
if (!eleInput) {
return;
}
// 有可能有删除之类的
const range = document.createRange();
const nodes = getNodeAndOffset(eleInput, obj.startIndex, obj.endIndex);
if (!nodes) {
return;
}
range.setStart(nodes[0], nodes[1]);
range.setEnd(nodes[2], nodes[3]);
// 包裹元素
const eleWrap = document.createElement('span');
eleWrap.setAttribute('data-gid', obj.gid);
eleWrap.className = 'word';
try {
range.surroundContents(eleWrap);
} catch (e) {
console.error('存在不可高亮的元素,多半由于选区交叉导致');
}
});
};
//--------
// 先请求划词数据
fetch('./cgi/data.json').then(res => res.json()).then(json => {
if (json.code == 1 && json.data) {
data.data = json.data;
// 高亮
doHighLight();
}
})
// ----------
// 划词事件处理
// 显示选区悬浮按钮
body.addEventListener('mouseup', function (event) {
const eleTarget = event.target;
hideSelectionPopover(popover);
if (eleTarget.closest && eleTarget.closest(selector)) {
showSelectionPopover(popover, eleTarget);
}
if (!eleTarget.closest('.word') && !comment.contains(eleTarget)) {
doRemoveActive();
}
});
// 浏览器尺寸改变时候,浮层的定位
window.addEventListener('resize', function () {
const selection = document.getSelection();
const eleClicked = selection.anchorNode.parentElement || selection.anchorNode;
let eleTarget = eleClicked.closest(selector) || eleClicked.querySelector(selector);
if (selection.toString().trim() && eleTarget) {
showSelectionPopover(popover, eleTarget);
}
});
// 点击评论按钮,显示评论输入框
popover.addEventListener('click', function () {
// 起止点和选区内容
form.sendData = getContentAndIndex(selector);
// 那就是如果起止点和选区内容之前就有,一模一样
// 直接显示评论
// ps: form.sendData 可能包含其他不需要的属性
// 比方说段落容器元素上还有 data-xxxid
// 则这里执行对比之前,需要先删除整个
// delete form.sendData.xxxid
// 或者下面使用其他的对比算法
// 例如 Object.keys(form.sendData) 改成 ['startIndex', 'endIndex', 'pid', 'selectContent']
let objCommMatch = null;
let isSameRange = data.data.some(obj => {
if (Object.keys(form.sendData).every(key => form.sendData[key] == obj[key])) {
objCommMatch = obj;
return true;
}
return false;
});
if (!isSameRange) {
// 选区高亮
doRangeWrapHighLight();
// 评论显示
comment.classList.add('active');
form.elements[0].disabled = form.elements[1].disabled = false;
textarea.focus();
} else if (objCommMatch) {
let eleWord = document.querySelector('.word[data-gid="' + objCommMatch.gid + '"]');
// 实际开发中,这里还应该包含对应评论高亮的逻辑
// 本演示省略
eleWord.click();
}
});
// 点击划词高亮
article.addEventListener('click', function (event) {
let eleTarget = event.target;
if (!eleTarget.classList.contains('word')) {
return;
}
// 如果当前已激活,不处理
if (eleTarget.classList.contains('active')) {
return;
}
const eleWordActive = document.querySelector('.word.active');
if (eleWordActive) {
eleWordActive.classList.remove('active');
}
eleTarget.classList.add('active');
// 评论显示
comment.classList.add('active');
// 对应的评论卡片高亮
// 本演示就单纯显示评论内容了
result.innerHTML = data.data.find(obj => obj.gid == eleTarget.dataset.gid).commentDetail.map(obj => `<li>${obj.content}</li>`).join('');
form.elements[0].disabled = form.elements[1].disabled = false;
});
// 取消划词的激活态
const doRemoveActive = function () {
// 取消激活态
const eleWrapActive = document.querySelector('.word.active');
if (eleWrapActive) {
eleWrapActive.classList.remove('active');
result.innerHTML = '';
form.elements[0].disabled = form.elements[1].disabled = true;
}
};
// 关闭评论侧边栏
shut.addEventListener('click', function () {
comment.classList.remove('active');
doRemoveActive();
});
// 评论极简示意
form.addEventListener('submit', function (event) {
event.preventDefault();
let eleWordActive = document.querySelector('.word.active');
if (!eleWordActive) {
return;
}
const content = form.elements[0].value.trim();
let gid = eleWordActive.dataset.gid;
// 请求的数据
let postData = {
...form.sendData
};
let objMatch = {};
// 如果是已有划词评论新增评论
if (gid != '0') {
objMatch = data.data.find(obj => obj.gid == gid);
postData = {
...objMatch
};
delete postData.commentDetail;
}
postData.content = content;
console.log('请求的数据是:', postData);
// 模拟请求
// 1. 按钮禁用
form.elements[1].disabled = true;
// 2. 发请求,这里定时器模拟
setTimeout(function () {
// 新数据
// 如果是数据驱动的框架
// 这里就会触发视图刷新
// 我们这里就不管了,随便示意下
const commentData = {
cid: 'cid' + setTimeout(0),
content: postData.content
};
// 新数据,随便用个 gid 代替下
if (gid == '0') {
gid = 'gid' + setTimeout(0);
// 新数据
data.data.push({
...postData,
gid: gid,
commentDetail: [commentData]
});
// 高亮选区的gid变化,不再是0
let eleWordActive = document.querySelector('.word.active');
eleWordActive.dataset.gid = gid;
} else {
objMatch.commentDetail.push(commentData);
}
// 评论显示
result.innerHTML = ( objMatch.commentDetail || [commentData]).map(obj => `<li>${obj.content}</li>`).join('');
// 按钮恢复
form.elements[1].disabled = false;
// 内容重置
form.reset();
delete form.sendData;
}, 20);
});
// 编辑内容时候,实时保存现在的划词起止位置和划词内容
document.querySelectorAll(selector).forEach(eleInput => {
eleInput.timerHandle = null;
eleInput.addEventListener('input', function () {
clearTimeout(this.timerHandle);
this.timerHandle = setTimeout(() => {
const arrWordList = getContentAndIndexList(this, '.word');
// 这就是划词评论新的数据
console.log(arrWordList);
// 正式开发场景下,这个数据需要保存到后端
// 然后,如果data.data 中的 gid 出现和 arrWordList 不匹配的
// 应该做相应的处理
// 这里就不演示了……
}, 300);
});
});
</script>
</html>
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化