Files
dsh f81d41c5ca 初始化:炫恒网络官网(五个页面)
站点:纯静态 HTML + CSS + 原生 JS,零框架零构建
页面:首页 / 合作品牌 / 荣誉资质 / 客户案例 / 关于我们

内容:
- site/          站点本体(含已压缩 webp 图片,约 4.8 MB)
- deploy/        Docker Compose / Caddy / Apache 部署配置
- _tools/        开发自检脚本与部署脚本
- Dockerfile     自建 nginx 镜像方案
- README.md      项目文档

设计:品牌黄 #ffc107 + 白,深蓝仅用于页脚;菜单长期置顶;
      交互动效只加在关键词与区块级元素上。

素材说明:原始素材(背景图/老网站/方案/荣誉资质/合作品牌,约 290 MB)
         未纳入版本控制,站点使用的是 site/assets 内已处理压缩的图片。
2026-10-13 16:12:41 +08:00

260 lines
9.6 KiB
JavaScript

/* 最小 DOM 仿真(供 tools/test-js.js 使用)
* 支持:元素树、classList、属性、事件、innerHTML 解析、querySelector(All)
* 目标:足以在 Node 里跑通站点 main.js 的交互逻辑。
*/
'use strict';
class ClassList {
constructor(el) { this.el = el; this.set = new Set(); }
add(...c) { c.forEach(x => x && this.set.add(x)); this.sync(); }
remove(...c) { c.forEach(x => this.set.delete(x)); this.sync(); }
contains(c) { return this.set.has(c); }
toggle(c, force) {
const on = force === undefined ? !this.set.has(c) : !!force;
if (on) this.set.add(c); else this.set.delete(c);
this.sync();
return on;
}
sync() { this.el._class = [...this.set].join(' '); }
}
class El {
constructor(tag, doc) {
this.tagName = String(tag || 'div').toUpperCase();
this.children = [];
this.parentNode = null;
this.attrs = {};
this.style = {};
this._class = '';
this._text = '';
this._html = '';
this.listeners = {};
this.offsetWidth = 0;
this.scrollHeight = 120;
this._value = '';
this.complete = false;
this.naturalWidth = 100;
Object.defineProperty(this, 'complete', {
get() { return !!this._complete; },
set(v) {
this._complete = !!v;
if (v) setTimeout(() => this.dispatch('load'), 0);
}
});
Object.defineProperty(this, 'naturalWidth', {
get() { return this._nw !== undefined ? this._nw : (this._complete ? 100 : 0); },
set(v) { this._nw = v; }
});
Object.defineProperty(this, 'naturalHeight', {
get() { return this._nh !== undefined ? this._nh : (this._complete ? 100 : 0); },
set(v) { this._nh = v; }
});
this.classList = new ClassList(this);
this.ownerDocument = doc || null;
}
/* 属性/IDL 反射,贴近浏览器行为 */
get src() { return this.attrs.src || ''; }
set src(v) {
this.attrs.src = String(v);
if (this.tagName === 'IMG') this.complete = true;
}
get href() { return this.attrs.href || ''; }
set href(v) { this.attrs.href = String(v); }
get id() { return this.attrs.id || ''; }
set id(v) { this.attrs.id = String(v); }
get type() { return this.attrs.type || ''; }
set type(v) { this.attrs.type = String(v); }
get name() { return this.attrs.name || ''; }
set name(v) { this.attrs.name = String(v); }
get value() { return this._value; }
set value(v) { this._value = String(v); this.attrs.value = String(v); }
get className() { return this._class; }
set className(v) { this._class = String(v); this.classList.set = new Set(this._class.split(/\s+/).filter(Boolean)); }
get textContent() { return this._text || this.children.map(c => c.textContent).join(''); }
set textContent(v) { this._text = String(v); this.children = []; }
get innerHTML() { return this._html; }
set innerHTML(v) { this._html = String(v); parseInto(this, this._html); }
get outerHTML() { return this._html; }
set outerHTML(v) { this._html = String(v); this._replaced = true; if (this.parentNode) this.parentNode._replacedChildren = true; }
appendChild(c) { c.parentNode = this; this.children.push(c); return c; }
cloneNode(deep) {
const copy = new El(this.tagName, this.ownerDocument);
copy.attrs = Object.assign({}, this.attrs);
copy._class = this._class;
copy.classList.set = new Set(this.classList.set);
copy._text = this._text;
copy._html = this._html;
copy._value = this._value;
if (deep) this.children.forEach(c => copy.appendChild(c.cloneNode(true)));
return copy;
}
removeChild(c) { this.children = this.children.filter(x => x !== c); return c; }
setAttribute(k, v) { this.attrs[k] = String(v); }
getAttribute(k) { return k in this.attrs ? this.attrs[k] : null; }
removeAttribute(k) { delete this.attrs[k]; }
hasAttribute(k) { return k in this.attrs; }
addEventListener(t, fn) { (this.listeners[t] = this.listeners[t] || []).push(fn); }
removeEventListener(t, fn) {
if (this.listeners[t]) this.listeners[t] = this.listeners[t].filter(f => f !== fn);
}
dispatch(t, ev) {
const evt = Object.assign({
type: t, target: this, preventDefault() { }, stopPropagation() { }
}, ev || {});
(this.listeners[t] || []).slice().forEach(fn => fn(evt));
if (this.parentNode && !evt._stopped) this.parentNode.dispatch(t, Object.assign(evt, { target: this }));
return evt;
}
closest(sel) {
let n = this;
while (n) { if (n.matches(sel)) return n; n = n.parentNode; }
return null;
}
focus() { this.ownerDocument && (this.ownerDocument.activeElement = this); }
scrollIntoView() { }
reset() {
(function walk(node) {
node.children.forEach(c => {
if (c.tagName === 'INPUT' || c.tagName === 'TEXTAREA' || c.tagName === 'SELECT') c.value = '';
walk(c);
});
})(this);
}
getBoundingClientRect() {
var w = this._boxW || 100, h = this._boxH || 100;
return { top: 0, left: 0, right: w, bottom: h, width: w, height: h };
}
matches(sel) { return matchCompound(this, sel); }
querySelector(sel) { return queryAll(this, sel)[0] || null; }
querySelectorAll(sel) { return queryAll(this, sel); }
get firstChild() { return this.children[0] || null; }
get nodeType() { return 1; }
}
/* ---------------------------------------------------------- 选择器 */
function matchSimple(el, sel) {
sel = sel.trim();
if (!sel) return true;
const parts = sel.match(/(^[a-zA-Z][\w-]*)|(\.[\w-]+)|(#[\[\]\w-]+)|(\[[^\]]+\])/g) || [];
for (const p of parts) {
if (p.startsWith('.')) { if (!el.classList.contains(p.slice(1))) return false; }
else if (p.startsWith('#')) { if (el.attrs.id !== p.slice(1)) return false; }
else if (p.startsWith('[')) {
const m = /^\[([\w-]+)(?:\s*([~|^$*]?=)\s*"?([^"\]]*)"?)?\]$/.exec(p);
if (!m) return false;
const [, name, op, val] = m;
if (!(name in el.attrs)) return false;
if (!op) continue;
const have = String(el.attrs[name]);
if (op === '=') { if (have !== val) return false; }
else if (op === '~=') { if (!have.split(/\s+/).includes(val)) return false; }
else if (op === '*=') { if (have.indexOf(val) < 0) return false; }
else if (op === '^=') { if (have.indexOf(val) !== 0) return false; }
else if (op === '$=') { if (!have.endsWith(val)) return false; }
} else if (el.tagName !== p.toUpperCase()) return false;
}
return true;
}
function splitDescendant(sel) {
const out = [];
let depth = 0, cur = '';
for (const ch of sel) {
if (ch === '[') depth++;
if (ch === ']') depth--;
if (/\s/.test(ch) && depth === 0) { if (cur) { out.push(cur); cur = ''; } }
else cur += ch;
}
if (cur) out.push(cur);
return out;
}
function matchCompound(el, sel) {
for (const group of sel.split(',')) {
const parts = splitDescendant(group);
if (!parts.length) continue;
if (!matchSimple(el, parts[parts.length - 1])) continue;
let i = parts.length - 2, n = el.parentNode, ok = true;
while (i >= 0) {
if (!n) { ok = false; break; }
if (matchSimple(n, parts[i])) i--;
n = n.parentNode;
}
if (ok && i < 0) return true;
}
return false;
}
function queryAll(root, selector) {
const out = [];
(function walk(node) {
for (const c of node.children) {
if (matchCompound(c, selector)) out.push(c);
walk(c);
}
})(root);
return out;
}
/* ---------------------------------------------------------- innerHTML 解析 */
const VOID = new Set(['img', 'br', 'hr', 'input', 'meta', 'link', 'source', 'area', 'base', 'col']);
function parseInto(root, html) {
root.children = [];
const stack = [root];
const re = /<!--[\s\S]*?-->|<\/([a-zA-Z][\w-]*)\s*>|<([a-zA-Z][\w-]*)((?:\s+[^<>]*?)?)\/?>|([^<]+)/g;
let m;
while ((m = re.exec(html))) {
if (m[0].startsWith('<!--')) continue;
if (m[1]) { // 闭合标签
const tag = m[1].toUpperCase();
for (let i = stack.length - 1; i > 0; i--) {
if (stack[i].tagName === tag) { stack.length = i; break; }
}
continue;
}
if (m[2]) { // 开标签
const tag = m[2];
const node = new El(tag, root.ownerDocument);
const attrStr = m[3] || '';
const ar = /([\w:-]+)(?:\s*=\s*"([^"]*)"|\s*=\s*'([^']*)'|\s*=\s*([^\s"'>]+))?/g;
let a;
while ((a = ar.exec(attrStr))) {
const name = a[1];
const val = a[2] !== undefined ? a[2] : a[3] !== undefined ? a[3] : a[4] !== undefined ? a[4] : '';
if (name === 'class') node.className = val;
else node.attrs[name] = val;
}
stack[stack.length - 1].appendChild(node);
if (!VOID.has(tag.toLowerCase()) && !m[0].endsWith('/>')) stack.push(node);
continue;
}
if (m[4]) { // 文本
const t = m[4];
if (t.trim()) stack[stack.length - 1]._text += t;
}
}
}
/* ---------------------------------------------------------- document */
function createDocument() {
const doc = new El('html', null);
doc.ownerDocument = doc;
doc.documentElement = doc;
doc.body = doc.appendChild(new El('body', doc));
doc.readyState = 'complete';
doc.hidden = false;
doc._l = {};
doc.activeElement = doc.body;
doc.createElement = t => new El(t, doc);
doc.addEventListener = El.prototype.addEventListener.bind(doc);
doc.removeEventListener = El.prototype.removeEventListener.bind(doc);
doc.dispatch = El.prototype.dispatch.bind(doc);
doc.querySelector = sel => queryAll(doc, sel)[0] || null;
doc.querySelectorAll = sel => queryAll(doc, sel);
doc.getElementById = id => queryAll(doc, '#' + id)[0] || null;
return doc;
}
module.exports = { El, createDocument, queryAll, matchCompound };