doohtml 0.98.9-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -0
- package/dist/doohtml.js +440 -0
- package/dist/doohtml.min.mjs +1 -0
- package/dist/doohtml.mjs +440 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# DooHTML
|
|
2
|
+
DooHTML is a lightweight rendering framework designed for efficient DOM manipulation and templating. Authored by Henrik Javen.
|
|
3
|
+
|
|
4
|
+
## Features
|
|
5
|
+
- **Template-based Rendering**: Use HTML templates to define reusable components.
|
|
6
|
+
- **Dynamic Data Binding**: Bind data to templates with `{{property}}` placeholders.
|
|
7
|
+
- **Lightweight and Fast**: Optimized for performance with minimal overhead.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install doohtml
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
Define a `<template>` with a `data-bind` row and `{{...}}` placeholders, and a target element with `data-template` and `data-key`. Then call `createTemplate` and `render` (or `add` to append).
|
|
18
|
+
|
|
19
|
+
**HTML:**
|
|
20
|
+
|
|
21
|
+
```html
|
|
22
|
+
<template id="item-tpl">
|
|
23
|
+
<div>
|
|
24
|
+
<div data-bind data-key="id">
|
|
25
|
+
<span class="name">{{name}}</span>
|
|
26
|
+
<span class="score">{{score}}</span>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</template>
|
|
30
|
+
<div data-template="item-tpl" data-key="id"></div>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**JavaScript:**
|
|
34
|
+
|
|
35
|
+
```javascript
|
|
36
|
+
import { createTemplate, render, add } from 'doohtml';
|
|
37
|
+
|
|
38
|
+
const data = [
|
|
39
|
+
{ id: 1, name: 'Alice', score: 10 },
|
|
40
|
+
{ id: 2, name: 'Bob', score: 20 }
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
// Create from template and replace content with data
|
|
44
|
+
const target = await createTemplate('item-tpl', data);
|
|
45
|
+
// target is the placeholder node; it now shows the rendered rows.
|
|
46
|
+
|
|
47
|
+
// Replace all rows
|
|
48
|
+
render(target, data);
|
|
49
|
+
|
|
50
|
+
// Or append more rows (e.g. for infinite scroll)
|
|
51
|
+
add(target, [{ id: 3, name: 'Carol', score: 30 }]);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Use `render(target, data)` to replace the list; use `add(target, data)` to append rows without clearing.
|
|
55
|
+
|
|
56
|
+
## API overview
|
|
57
|
+
|
|
58
|
+
- **createTemplate**(id, data?, src?) – Creates a template instance from an element id (or selector), optionally renders initial data, returns the target node.
|
|
59
|
+
- **render**(target, data, start?) – Replaces target content with rows from `data`.
|
|
60
|
+
- **add**(target, dataSet, start?) – Appends rows from `dataSet` to the target.
|
|
61
|
+
- **renderWithProvider**(target, dataList, start?, length?, dataProvider?) – Replace content using a data list and optional provider (default: `(i, list) => list[i]`).
|
|
62
|
+
- **addWithProvider**(target, dataList, start?, length?, dataProvider?) – Append rows using a data list and optional provider.
|
|
63
|
+
- **prefetchTemplate**(src) – Preload a template from a URL.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Development
|
|
68
|
+
|
|
69
|
+
### Prerequisites
|
|
70
|
+
|
|
71
|
+
- **Node.js** (v12 or higher)
|
|
72
|
+
- **npm**
|
|
73
|
+
|
|
74
|
+
### Build
|
|
75
|
+
|
|
76
|
+
From the project directory:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npm install
|
|
80
|
+
npm run build
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
This runs `build:copy` and `build:min`:
|
|
84
|
+
|
|
85
|
+
- **build:copy** – Copies `src/doohtml.js` to `dist/doohtml.js` and `dist/doohtml.mjs`.
|
|
86
|
+
- **build:min** – Minifies `src/doohtml.js` with Terser (preserving quoted properties) to `dist/doohtml.min.mjs`.
|
|
87
|
+
|
|
88
|
+
So `dist/` contains: `doohtml.js`, `doohtml.mjs`, and `doohtml.min.mjs`.
|
|
89
|
+
|
|
90
|
+
### Project structure
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
doohtml/
|
|
94
|
+
├── src/
|
|
95
|
+
│ └── doohtml.js # Main source
|
|
96
|
+
├── dist/
|
|
97
|
+
│ ├── doohtml.js # ESM copy (require)
|
|
98
|
+
│ ├── doohtml.mjs # ESM copy (import)
|
|
99
|
+
│ └── doohtml.min.mjs # Minified ESM
|
|
100
|
+
├── package.json
|
|
101
|
+
└── README.md
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Other scripts
|
|
105
|
+
|
|
106
|
+
- **preview** – `npm run build && npm pack` (creates a tarball for local testing).
|
|
107
|
+
- **prepublishOnly** – Runs `npm run build` before publish.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
This project is licensed under the MIT License. See the `LICENSE` file for details.
|
package/dist/doohtml.js
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
const Config = {
|
|
2
|
+
NAME:'DooHTML',
|
|
3
|
+
DATA_BIND:'data-bind',
|
|
4
|
+
DATA_TEMPLATE:'data-template',
|
|
5
|
+
MATCH:{ANY:-1,STARTS_WITH:0,EXACT:1},
|
|
6
|
+
DELIMITER:{'BEG':'{{','END':'}}'},
|
|
7
|
+
DATA_KEY:'data-key',
|
|
8
|
+
KEY:'key'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const {cloneNode, appendChild} = globalThis.Node.prototype
|
|
12
|
+
const cloneDeep = n => cloneNode.call(n, true)
|
|
13
|
+
const appendRow = function(child) {
|
|
14
|
+
return appendChild.call(this, child);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const version = 'v0.98.9-beta.0'
|
|
18
|
+
|
|
19
|
+
const getItemValue = (item, prop) => {
|
|
20
|
+
if (!prop.includes('.')) {
|
|
21
|
+
return item[prop] ? item[prop] : ''
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return prop.split('.').reduce((acc, key) => {
|
|
25
|
+
return acc && acc[key] !== undefined ? acc[key] : ''
|
|
26
|
+
}, item)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const getNode = (node, arr) => arr.reduce((currentNode, index) => currentNode?.childNodes[index] || null, node)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
const isTable = (node) => {
|
|
33
|
+
if (!node || !node.tagName) return false
|
|
34
|
+
return ['TABLE','TBODY','THEAD','TFOOT','TR','TH'].includes(node.tagName)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const setNodeValues = (node, dataItem, dataSlots) => {
|
|
38
|
+
const len = dataSlots.length
|
|
39
|
+
for (let x = 0; x < len; x++) {
|
|
40
|
+
const curNode = getNode(node, dataSlots[x][1], 0)
|
|
41
|
+
if (curNode) {
|
|
42
|
+
if (dataSlots[x][2] === 'textContent') {
|
|
43
|
+
curNode.nodeValue = dataItem[dataSlots[x][0]]
|
|
44
|
+
} else {
|
|
45
|
+
curNode.setAttribute(dataSlots[x][2], dataItem[dataSlots[x][0]])
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
globalThis.console.log('Field:' + dataSlots[x][0] + ' does not exist')
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const render = (target, data, start = 0) => {
|
|
54
|
+
if (data.length === 0) {
|
|
55
|
+
target.textContent = ''
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
renderHTML(target, data, start)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const renderHTML = (target, data, start = 0, end=null) => {
|
|
62
|
+
let dataLen = data.length
|
|
63
|
+
|
|
64
|
+
let stop = end ? start + dataLen : dataLen - start
|
|
65
|
+
if (stop > dataLen) { stop = dataLen }
|
|
66
|
+
|
|
67
|
+
// Clear existing content
|
|
68
|
+
target.textContent = ''
|
|
69
|
+
const insertRow = appendRow.bind(target)
|
|
70
|
+
const parentElement = target.parentElement
|
|
71
|
+
const key = target[Config.KEY]
|
|
72
|
+
const childNodes = target.childNodes
|
|
73
|
+
const isTableSection = target.tagName === 'TBODY' || target.tagName === 'THEAD' || target.tagName === 'TFOOT'
|
|
74
|
+
if (!isTableSection) {
|
|
75
|
+
target.remove()
|
|
76
|
+
}
|
|
77
|
+
let fragment = globalThis.document.createDocumentFragment()
|
|
78
|
+
if (childNodes.length > 0) {
|
|
79
|
+
for (let i = start; i < stop; ++i) {
|
|
80
|
+
setNodeValues(target.processNode, data[i], target.dataSlots)
|
|
81
|
+
let cloned = cloneDeep(target.processNode)
|
|
82
|
+
cloned[Config.KEY] = getItemValue(data[i],key)
|
|
83
|
+
fragment.appendChild(cloned)
|
|
84
|
+
}
|
|
85
|
+
if (isTableSection) {
|
|
86
|
+
target.appendChild(fragment)
|
|
87
|
+
} else {
|
|
88
|
+
parentElement.appendChild(fragment)
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
for (let i = start; i < stop; ++i) {
|
|
92
|
+
setNodeValues(target.processNode, data[i], target.dataSlots)
|
|
93
|
+
let cloned = cloneDeep(target.processNode)
|
|
94
|
+
cloned[Config.KEY] = getItemValue(data[i],key)
|
|
95
|
+
insertRow(cloned)
|
|
96
|
+
}
|
|
97
|
+
if (!isTableSection) {
|
|
98
|
+
parentElement.appendChild(target)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const renderHTMLWithProvider = (target, dataProvider, start = 0, length = null, rows = []) => {
|
|
104
|
+
if (length === null || length === 0) {
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const stop = start + length
|
|
109
|
+
const key = target[Config.KEY]
|
|
110
|
+
const insertRow = appendRow.bind(target)
|
|
111
|
+
|
|
112
|
+
const table = target.parentElement
|
|
113
|
+
const wasAttached = table && table.contains(target)
|
|
114
|
+
|
|
115
|
+
if (wasAttached) {
|
|
116
|
+
target.remove()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
for (let i = start; i < stop; ++i) {
|
|
120
|
+
const dataItem = dataProvider(i, rows)
|
|
121
|
+
setNodeValues(target.processNode, dataItem, target.dataSlots)
|
|
122
|
+
let cloned = cloneDeep(target.processNode)
|
|
123
|
+
cloned[Config.KEY] = getItemValue(dataItem, key)
|
|
124
|
+
insertRow(cloned)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (wasAttached && table) {
|
|
128
|
+
table.append(target)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const defaultProvider = (i, list) => list[i]
|
|
133
|
+
|
|
134
|
+
const renderWithProvider = (target, dataList, start = 0, length = null, dataProvider = null) => {
|
|
135
|
+
const provider = dataProvider ?? defaultProvider
|
|
136
|
+
const len = length ?? (dataList?.length ?? 0) - start
|
|
137
|
+
if (len <= 0) return
|
|
138
|
+
renderHTMLWithProvider(target, provider, start, len, dataList ?? [])
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const add = (target, dataSet, start=0) => {
|
|
142
|
+
renderHTML(target, dataSet, start , dataSet.length - start)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const addWithProvider = (target, dataList, start = 0, length = null, dataProvider = null) => {
|
|
146
|
+
const provider = dataProvider ?? defaultProvider
|
|
147
|
+
const len = length ?? (dataList?.length ?? 0) - start
|
|
148
|
+
if (len <= 0) return
|
|
149
|
+
|
|
150
|
+
const stop = start + len
|
|
151
|
+
const key = target[Config.KEY]
|
|
152
|
+
const insertRow = appendRow.bind(target)
|
|
153
|
+
const rows = dataList ?? []
|
|
154
|
+
|
|
155
|
+
// Simple loop - append to existing content (keep attached)
|
|
156
|
+
for (let i = start; i < stop; ++i) {
|
|
157
|
+
// Call provider to get single data object for this index
|
|
158
|
+
const dataItem = provider(i, rows)
|
|
159
|
+
|
|
160
|
+
// Set values and clone the process node
|
|
161
|
+
setNodeValues(target.processNode, dataItem, target.dataSlots)
|
|
162
|
+
let cloned = cloneDeep(target.processNode)
|
|
163
|
+
cloned[Config.KEY] = getItemValue(dataItem, key)
|
|
164
|
+
insertRow(cloned)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @deprecated Use add() instead. Will be removed in a future version.
|
|
170
|
+
*/
|
|
171
|
+
const append = (target, dataSet, start = 0) => {
|
|
172
|
+
if (typeof globalThis !== 'undefined' && globalThis.console?.warn) {
|
|
173
|
+
globalThis.console.warn('DooHTML: append() is deprecated. Use add() instead.')
|
|
174
|
+
}
|
|
175
|
+
return add(target, dataSet, start)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* @deprecated Use addWithProvider() instead. Will be removed in a future version.
|
|
180
|
+
*/
|
|
181
|
+
const appendWithProvider = (target, dataList, start = 0, length = null, dataProvider = null) => {
|
|
182
|
+
if (typeof globalThis !== 'undefined' && globalThis.console?.warn) {
|
|
183
|
+
globalThis.console.warn('DooHTML: appendWithProvider() is deprecated. Use addWithProvider() instead.')
|
|
184
|
+
}
|
|
185
|
+
return addWithProvider(target, dataList, start, length, dataProvider)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const dooParse = (argDataNode) => {
|
|
189
|
+
const _xAttr = ['src', 'selected', 'checked', 'disabled', 'readonly']
|
|
190
|
+
|
|
191
|
+
let tplNode = cloneDeep(argDataNode)
|
|
192
|
+
tplNode.removeAttribute(Config.DATA_BIND)
|
|
193
|
+
delete tplNode.dataset.key
|
|
194
|
+
let htmlStr = tplNode.outerHTML.replaceAll('\t', '').replaceAll('\n', '')
|
|
195
|
+
let orgStr = htmlStr
|
|
196
|
+
_xAttr.forEach(item => {
|
|
197
|
+
htmlStr = htmlStr.replaceAll(new RegExp(' ' + item + '="{{(.+)}}"', 'g'), ' doo-' + item + '="{{$1}}"')
|
|
198
|
+
})
|
|
199
|
+
let xHtml = (orgStr === htmlStr)
|
|
200
|
+
|
|
201
|
+
let elem = globalThis.document.createElement('template')
|
|
202
|
+
elem.innerHTML = htmlStr
|
|
203
|
+
let dataSlots = []
|
|
204
|
+
|
|
205
|
+
const addDataSlot = (item, fld, type) => {
|
|
206
|
+
let slot = []
|
|
207
|
+
let child = item
|
|
208
|
+
while (child !== elem.firstElementChild) {
|
|
209
|
+
let prev = child.previousSibling
|
|
210
|
+
let cnt = 0
|
|
211
|
+
|
|
212
|
+
while (prev) {
|
|
213
|
+
cnt++
|
|
214
|
+
prev = prev.previousSibling
|
|
215
|
+
}
|
|
216
|
+
child = child.parentNode
|
|
217
|
+
if (child) {
|
|
218
|
+
slot.unshift(cnt)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
dataSlots.push([fld,slot.slice(1),type])
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const textWalker = globalThis.document.createTreeWalker(
|
|
225
|
+
elem.content,
|
|
226
|
+
globalThis.NodeFilter.SHOW_TEXT,
|
|
227
|
+
{
|
|
228
|
+
acceptNode() {
|
|
229
|
+
return globalThis.NodeFilter.FILTER_ACCEPT
|
|
230
|
+
},
|
|
231
|
+
}
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
let textNode = textWalker.nextNode()
|
|
235
|
+
let multiText = []
|
|
236
|
+
while (textNode) {
|
|
237
|
+
let val = textNode.wholeText.trim()
|
|
238
|
+
if (val.indexOf('{{') === 0 && val.lastIndexOf('}}') === val.length-2) {
|
|
239
|
+
//do nothing
|
|
240
|
+
} else {
|
|
241
|
+
let text = val.replaceAll('{{', '<span>{{').replaceAll('}}', '}}</span>')
|
|
242
|
+
multiText.push({node:textNode.parentNode, oldText:val, newText:text})
|
|
243
|
+
|
|
244
|
+
}
|
|
245
|
+
textNode = textWalker.nextNode()
|
|
246
|
+
}
|
|
247
|
+
for (let i=0, len = multiText.length; i<len; i++) {
|
|
248
|
+
multiText[i].node.innerHTML = multiText[i].node.innerHTML.replace(multiText[i].oldText,multiText[i].newText)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let processedElem = cloneDeep(elem.content)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
const treeWalker = globalThis.document.createTreeWalker(
|
|
255
|
+
processedElem,
|
|
256
|
+
globalThis.NodeFilter.SHOW_TEXT,
|
|
257
|
+
{
|
|
258
|
+
acceptNode() {
|
|
259
|
+
return globalThis.NodeFilter.FILTER_ACCEPT
|
|
260
|
+
},
|
|
261
|
+
}
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
let currentNode = treeWalker.nextNode()
|
|
265
|
+
while (currentNode) {
|
|
266
|
+
const matches = currentNode.nodeValue.match(/\{\{(.*?)\}\}/g)
|
|
267
|
+
if (matches) {
|
|
268
|
+
const parent = currentNode.parentNode
|
|
269
|
+
matches.forEach((match) => {
|
|
270
|
+
const fld = match.replaceAll(/\{\{|\}\}/g, '').trim()
|
|
271
|
+
const textNode = globalThis.document.createTextNode(fld)
|
|
272
|
+
currentNode.textContent = ''
|
|
273
|
+
const newNode = parent.appendChild(textNode)
|
|
274
|
+
addDataSlot(newNode, fld, 'textContent')
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
currentNode = treeWalker.nextNode()
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const elemWalker = globalThis.document.createTreeWalker(
|
|
282
|
+
processedElem,
|
|
283
|
+
globalThis.NodeFilter.SHOW_ELEMENT,
|
|
284
|
+
{
|
|
285
|
+
acceptNode() {
|
|
286
|
+
return globalThis.NodeFilter.FILTER_ACCEPT
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
currentNode = elemWalker.nextNode()
|
|
292
|
+
while (currentNode) {
|
|
293
|
+
for (const attr of currentNode.attributes) {
|
|
294
|
+
if (attr.nodeValue.includes('{{')) {
|
|
295
|
+
addDataSlot(currentNode, attr.nodeValue.replace('{{','').replace('}}',''), attr.name)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
currentNode = elemWalker.nextNode()
|
|
299
|
+
}
|
|
300
|
+
let templateStr = processedElem.firstElementChild.outerHTML
|
|
301
|
+
dataSlots.forEach(item=>{
|
|
302
|
+
let str = '{{' + item[0] + '}}'
|
|
303
|
+
templateStr = templateStr.replaceAll(new RegExp(str,'g'),'')
|
|
304
|
+
|
|
305
|
+
})
|
|
306
|
+
processedElem.outerHTML = templateStr
|
|
307
|
+
|
|
308
|
+
return {processNode:processedElem.firstElementChild, xHtml, dataSlots}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const fetchTemplate = (url) => {
|
|
312
|
+
return new Promise((resolve, reject) => {
|
|
313
|
+
const xhr = new XMLHttpRequest()
|
|
314
|
+
xhr.open("GET", url)
|
|
315
|
+
xhr.addEventListener('load', () => resolve(xhr.responseText))
|
|
316
|
+
xhr.onerror = () => reject(xhr.statusText)
|
|
317
|
+
xhr.send()
|
|
318
|
+
})
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const setReactiveDataNodes = (tplNode) => {
|
|
322
|
+
const place = []
|
|
323
|
+
const getNodeLevel = (node) => {
|
|
324
|
+
let level = 0
|
|
325
|
+
while (node.parentElement) {
|
|
326
|
+
node = node.parentElement
|
|
327
|
+
level++
|
|
328
|
+
}
|
|
329
|
+
return level
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const processReactiveElements = (reactiveElems) => {
|
|
333
|
+
const orderedElems = [...reactiveElems]
|
|
334
|
+
.map((elem) => ({
|
|
335
|
+
elem,
|
|
336
|
+
level: getNodeLevel(elem),
|
|
337
|
+
useParent: elem.dataset.src?.startsWith('this.parent'),
|
|
338
|
+
noRepeat: Object.hasOwn(elem.dataset, 'norepeat'),
|
|
339
|
+
}))
|
|
340
|
+
.sort((a, b) => b.level - a.level)
|
|
341
|
+
|
|
342
|
+
orderedElems.forEach(({ elem, level, useParent, noRepeat }, index) => {
|
|
343
|
+
const parent = elem.parentElement
|
|
344
|
+
// Keep TBODY/THEAD/TFOOT as container so TABLE keeps thead when data-bind is on tbody
|
|
345
|
+
const isTableSection = parent?.tagName === 'TABLE' && '|TBODY|THEAD|TFOOT|'.includes(`|${elem.tagName}|`)
|
|
346
|
+
let dataElem = isTableSection
|
|
347
|
+
? elem
|
|
348
|
+
: '|STYLE|LINK|'.includes(`|${elem.tagName}|`)
|
|
349
|
+
? elem
|
|
350
|
+
: parent && '|DL|UL|TBODY|THEAD|TFOOT|TR|SELECT|SECTION|'.includes(`|${parent.tagName}|`)
|
|
351
|
+
? parent
|
|
352
|
+
: parent || elem // Fallback to elem if parentElement is null
|
|
353
|
+
|
|
354
|
+
// Ensure dataElem is never null
|
|
355
|
+
if (!dataElem) {
|
|
356
|
+
console.warn('dataElem is null, using elem as fallback')
|
|
357
|
+
dataElem = elem
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const parsedNode = dooParse(elem)
|
|
361
|
+
Object.assign(dataElem, {
|
|
362
|
+
processNode: parsedNode.processNode,
|
|
363
|
+
xHtml: parsedNode.xHtml,
|
|
364
|
+
dataSlots: parsedNode.dataSlots,
|
|
365
|
+
name: index,
|
|
366
|
+
level,
|
|
367
|
+
useParent,
|
|
368
|
+
noRepeat,
|
|
369
|
+
isTable: isTable(dataElem),
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
if (dataElem.tagName === 'DATA' || dataElem.tagName === 'STYLE' || dataElem.tagName === 'LINK') {
|
|
373
|
+
elem.parentElement?.replaceChild(dataElem, elem) || console.warn('Templates should only have one child node')
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
place.push(dataElem)
|
|
377
|
+
})
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const reactiveElems = tplNode.content.querySelectorAll(`[${Config.DATA_BIND}]`)
|
|
381
|
+
reactiveElems.forEach((elem) => {
|
|
382
|
+
if (!Object.hasOwn(elem.dataset, 'src')) {
|
|
383
|
+
elem.dataset.src = tplNode.hasAttribute('doo-dispatch') ? 'DooX' : Config.DATA_BIND
|
|
384
|
+
}
|
|
385
|
+
delete elem.dataset.src
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
processReactiveElements(reactiveElems)
|
|
389
|
+
|
|
390
|
+
tplNode.place = place
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
const prefetchTemplate = async (src) => {
|
|
395
|
+
if (src && (src.startsWith('./') || src.startsWith('../') || src.startsWith('http'))) {
|
|
396
|
+
const tpl = await fetchTemplate(src)
|
|
397
|
+
return tpl
|
|
398
|
+
}
|
|
399
|
+
return null
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const createTemplate = async (id, data = [], src = null) => {
|
|
403
|
+
|
|
404
|
+
let tpl = src ? await prefetchTemplate(src) : ''
|
|
405
|
+
if (!tpl) {
|
|
406
|
+
if (id.startsWith('<')) {
|
|
407
|
+
tpl = id
|
|
408
|
+
} else if (id.startsWith('#')) {
|
|
409
|
+
tpl = globalThis.document.querySelector(id).outerHTML
|
|
410
|
+
} else {
|
|
411
|
+
tpl = globalThis.document.querySelector('#' + id).outerHTML
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const elem = globalThis.document.createElement('div')
|
|
416
|
+
elem.innerHTML = tpl
|
|
417
|
+
if (elem.querySelector('template')) {
|
|
418
|
+
elem.innerHTML = elem.querySelector('template')
|
|
419
|
+
? tpl
|
|
420
|
+
: `<template><center><pre>The template you are trying to import does not have a <template> tag</pre><div style="color:red">${tpl}</div></center></template>`
|
|
421
|
+
}
|
|
422
|
+
const importedTemplate = cloneDeep(elem.querySelector('template'))
|
|
423
|
+
|
|
424
|
+
const templateNode = globalThis.document.createElement('template')
|
|
425
|
+
importedTemplate.removeAttribute('id')
|
|
426
|
+
templateNode.innerHTML = importedTemplate.innerHTML
|
|
427
|
+
|
|
428
|
+
setReactiveDataNodes(templateNode)
|
|
429
|
+
const subscriber = globalThis.document.querySelector(`[data-template="${id}"]`)
|
|
430
|
+
templateNode["place"][0].textContent = ''
|
|
431
|
+
templateNode["place"][0][Config.KEY] = subscriber.dataset[Config.KEY]
|
|
432
|
+
subscriber.parentElement.replaceChild(templateNode.content, subscriber)
|
|
433
|
+
|
|
434
|
+
if (data.length > 0) {
|
|
435
|
+
render(templateNode["place"][0], data, 0)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return templateNode["place"][0]
|
|
439
|
+
}
|
|
440
|
+
export { createTemplate, add, addWithProvider, append, appendWithProvider, render, renderWithProvider, Config, version, prefetchTemplate }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const Config={t:"DooHTML",l:"data-bind",o:"data-template",i:{T:-1,p:0,h:1},u:{BEG:"{{",END:"}}"},m:"data-key",D:"key"},{cloneNode:cloneNode,appendChild:appendChild}=globalThis.Node.prototype,cloneDeep=e=>cloneNode.call(e,!0),appendRow=function(e){return appendChild.call(this,e)},version="v0.98.9-beta.0",getItemValue=(e,t)=>t.includes(".")?t.split(".").reduce(((e,t)=>e&&void 0!==e[t]?e[t]:""),e):e[t]?e[t]:"",getNode=(e,t)=>t.reduce(((e,t)=>e?.childNodes[t]||null),e),isTable=e=>!(!e||!e.tagName)&&["TABLE","TBODY","THEAD","TFOOT","TR","TH"].includes(e.tagName),setNodeValues=(e,t,l)=>{const o=l.length;for(let a=0;a<o;a++){const o=getNode(e,l[a][1]);o?"textContent"===l[a][2]?o.nodeValue=t[l[a][0]]:o.setAttribute(l[a][2],t[l[a][0]]):globalThis.console.log("Field:"+l[a][0]+" does not exist")}},render=(e,t,l=0)=>{0!==t.length?renderHTML(e,t,l):e.textContent=""},renderHTML=(e,t,l=0,o=null)=>{let a=t.length,n=o?l+a:a-l;n>a&&(n=a),e.textContent="";const d=appendRow.bind(e),s=e.parentElement,r=e[Config.D],i=e.childNodes,T="TBODY"===e.tagName||"THEAD"===e.tagName||"TFOOT"===e.tagName;T||e.remove();let p=globalThis.document.createDocumentFragment();if(i.length>0){for(let o=l;o<n;++o){setNodeValues(e.v,t[o],e.N);let l=cloneDeep(e.v);l[Config.D]=getItemValue(t[o],r),p.appendChild(l)}T?e.appendChild(p):s.appendChild(p)}else{for(let o=l;o<n;++o){setNodeValues(e.v,t[o],e.N);let l=cloneDeep(e.v);l[Config.D]=getItemValue(t[o],r),d(l)}T||s.appendChild(e)}},renderHTMLWithProvider=(e,t,l=0,o=null,a=[])=>{if(null===o||0===o)return;const n=l+o,d=e[Config.D],s=appendRow.bind(e),r=e.parentElement,i=r&&r.contains(e);i&&e.remove();for(let o=l;o<n;++o){const l=t(o,a);setNodeValues(e.v,l,e.N);let n=cloneDeep(e.v);n[Config.D]=getItemValue(l,d),s(n)}i&&r&&r.append(e)},defaultProvider=(e,t)=>t[e],renderWithProvider=(e,t,l=0,o=null,a=null)=>{const n=a??defaultProvider,d=o??(t?.length??0)-l;d<=0||renderHTMLWithProvider(e,n,l,d,t??[])},add=(e,t,l=0)=>{renderHTML(e,t,l,t.length-l)},addWithProvider=(e,t,l=0,o=null,a=null)=>{const n=a??defaultProvider,d=o??(t?.length??0)-l;if(d<=0)return;const s=l+d,r=e[Config.D],i=appendRow.bind(e),T=t??[];for(let t=l;t<s;++t){const l=n(t,T);setNodeValues(e.v,l,e.N);let o=cloneDeep(e.v);o[Config.D]=getItemValue(l,r),i(o)}},append=(e,t,l=0)=>("undefined"!=typeof globalThis&&globalThis.console?.warn&&globalThis.console.warn("DooHTML: append() is deprecated. Use add() instead."),add(e,t,l)),appendWithProvider=(e,t,l=0,o=null,a=null)=>("undefined"!=typeof globalThis&&globalThis.console?.warn&&globalThis.console.warn("DooHTML: appendWithProvider() is deprecated. Use addWithProvider() instead."),addWithProvider(e,t,l,o,a)),dooParse=e=>{let t=cloneDeep(e);t.removeAttribute(Config.l),delete t.dataset.key;let l=t.outerHTML.replaceAll("\t","").replaceAll("\n",""),o=l;["src","selected","checked","disabled","readonly"].forEach((e=>{l=l.replaceAll(new RegExp(" "+e+'="{{(.+)}}"',"g")," doo-"+e+'="{{$1}}"')}));let a=o===l,n=globalThis.document.createElement("template");n.innerHTML=l;let d=[];const s=(e,t,l)=>{let o=[],a=e;for(;a!==n.firstElementChild;){let e=a.previousSibling,t=0;for(;e;)t++,e=e.previousSibling;a=a.parentNode,a&&o.unshift(t)}d.push([t,o.slice(1),l])},r=globalThis.document.createTreeWalker(n.content,globalThis.NodeFilter.SHOW_TEXT,{acceptNode:()=>globalThis.NodeFilter.FILTER_ACCEPT});let i=r.nextNode(),T=[];for(;i;){let e=i.wholeText.trim();if(0===e.indexOf("{{")&&e.lastIndexOf("}}")===e.length-2);else{let t=e.replaceAll("{{","<span>{{").replaceAll("}}","}}</span>");T.push({node:i.parentNode,C:e,P:t})}i=r.nextNode()}for(let e=0,t=T.length;e<t;e++)T[e].node.innerHTML=T[e].node.innerHTML.replace(T[e].C,T[e].P);let p=cloneDeep(n.content);const c=globalThis.document.createTreeWalker(p,globalThis.NodeFilter.SHOW_TEXT,{acceptNode:()=>globalThis.NodeFilter.FILTER_ACCEPT});let g=c.nextNode();for(;g;){const e=g.nodeValue.match(/\{\{(.*?)\}\}/g);if(e){const t=g.parentNode;e.forEach((e=>{const l=e.replaceAll(/\{\{|\}\}/g,"").trim(),o=globalThis.document.createTextNode(l);g.textContent="";const a=t.appendChild(o);s(a,l,"textContent")}))}g=c.nextNode()}const h=globalThis.document.createTreeWalker(p,globalThis.NodeFilter.SHOW_ELEMENT,{acceptNode:()=>globalThis.NodeFilter.FILTER_ACCEPT});for(g=h.nextNode();g;){for(const e of g.attributes)e.nodeValue.includes("{{")&&s(g,e.nodeValue.replace("{{","").replace("}}",""),e.name);g=h.nextNode()}let f=p.firstElementChild.outerHTML;return d.forEach((e=>{let t="{{"+e[0]+"}}";f=f.replaceAll(new RegExp(t,"g"),"")})),p.outerHTML=f,{v:p.firstElementChild,A:a,N:d}},fetchTemplate=e=>new Promise(((t,l)=>{const o=new XMLHttpRequest;o.open("GET",e),o.addEventListener("load",(()=>t(o.responseText))),o.onerror=()=>l(o.statusText),o.send()})),setReactiveDataNodes=e=>{const t=[],l=e=>{let t=0;for(;e.parentElement;)e=e.parentElement,t++;return t},o=e.content.querySelectorAll(`[${Config.l}]`);o.forEach((t=>{Object.hasOwn(t.dataset,"src")||(t.dataset.src=e.hasAttribute("doo-dispatch")?"DooX":Config.l),delete t.dataset.src})),(e=>{[...e].map((e=>({L:e,level:l(e),H:e.dataset.src?.startsWith("this.parent"),O:Object.hasOwn(e.dataset,"norepeat")}))).sort(((e,t)=>t.level-e.level)).forEach((({L:e,level:l,H:o,O:a},n)=>{const d=e.parentElement;let s="TABLE"===d?.tagName&&"|TBODY|THEAD|TFOOT|".includes(`|${e.tagName}|`)||"|STYLE|LINK|".includes(`|${e.tagName}|`)?e:d&&"|DL|UL|TBODY|THEAD|TFOOT|TR|SELECT|SECTION|".includes(`|${d.tagName}|`)?d:d||e;s||(console.warn("dataElem is null, using elem as fallback"),s=e);const r=dooParse(e);Object.assign(s,{v:r.v,A:r.A,N:r.N,name:n,level:l,H:o,O:a,R:isTable(s)}),"DATA"!==s.tagName&&"STYLE"!==s.tagName&&"LINK"!==s.tagName||e.parentElement?.replaceChild(s,e)||console.warn("Templates should only have one child node"),t.push(s)}))})(o),e.place=t},prefetchTemplate=async e=>{if(e&&(e.startsWith("./")||e.startsWith("../")||e.startsWith("http"))){return await fetchTemplate(e)}return null},createTemplate=async(e,t=[],l=null)=>{let o=l?await prefetchTemplate(l):"";o||(o=e.startsWith("<")?e:e.startsWith("#")?globalThis.document.querySelector(e).outerHTML:globalThis.document.querySelector("#"+e).outerHTML);const a=globalThis.document.createElement("div");a.innerHTML=o,a.querySelector("template")&&(a.innerHTML=a.querySelector("template")?o:`<template><center><pre>The template you are trying to import does not have a <template> tag</pre><div style="color:red">${o}</div></center></template>`);const n=cloneDeep(a.querySelector("template")),d=globalThis.document.createElement("template");n.removeAttribute("id"),d.innerHTML=n.innerHTML,setReactiveDataNodes(d);const s=globalThis.document.querySelector(`[data-template="${e}"]`);return d.place[0].textContent="",d.place[0][Config.D]=s.dataset[Config.D],s.parentElement.replaceChild(d.content,s),t.length>0&&render(d.place[0],t,0),d.place[0]};export{createTemplate,add,addWithProvider,append,appendWithProvider,render,renderWithProvider,Config,version,prefetchTemplate};
|
package/dist/doohtml.mjs
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
const Config = {
|
|
2
|
+
NAME:'DooHTML',
|
|
3
|
+
DATA_BIND:'data-bind',
|
|
4
|
+
DATA_TEMPLATE:'data-template',
|
|
5
|
+
MATCH:{ANY:-1,STARTS_WITH:0,EXACT:1},
|
|
6
|
+
DELIMITER:{'BEG':'{{','END':'}}'},
|
|
7
|
+
DATA_KEY:'data-key',
|
|
8
|
+
KEY:'key'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const {cloneNode, appendChild} = globalThis.Node.prototype
|
|
12
|
+
const cloneDeep = n => cloneNode.call(n, true)
|
|
13
|
+
const appendRow = function(child) {
|
|
14
|
+
return appendChild.call(this, child);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const version = 'v0.98.9-beta.0'
|
|
18
|
+
|
|
19
|
+
const getItemValue = (item, prop) => {
|
|
20
|
+
if (!prop.includes('.')) {
|
|
21
|
+
return item[prop] ? item[prop] : ''
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return prop.split('.').reduce((acc, key) => {
|
|
25
|
+
return acc && acc[key] !== undefined ? acc[key] : ''
|
|
26
|
+
}, item)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const getNode = (node, arr) => arr.reduce((currentNode, index) => currentNode?.childNodes[index] || null, node)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
const isTable = (node) => {
|
|
33
|
+
if (!node || !node.tagName) return false
|
|
34
|
+
return ['TABLE','TBODY','THEAD','TFOOT','TR','TH'].includes(node.tagName)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const setNodeValues = (node, dataItem, dataSlots) => {
|
|
38
|
+
const len = dataSlots.length
|
|
39
|
+
for (let x = 0; x < len; x++) {
|
|
40
|
+
const curNode = getNode(node, dataSlots[x][1], 0)
|
|
41
|
+
if (curNode) {
|
|
42
|
+
if (dataSlots[x][2] === 'textContent') {
|
|
43
|
+
curNode.nodeValue = dataItem[dataSlots[x][0]]
|
|
44
|
+
} else {
|
|
45
|
+
curNode.setAttribute(dataSlots[x][2], dataItem[dataSlots[x][0]])
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
globalThis.console.log('Field:' + dataSlots[x][0] + ' does not exist')
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const render = (target, data, start = 0) => {
|
|
54
|
+
if (data.length === 0) {
|
|
55
|
+
target.textContent = ''
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
renderHTML(target, data, start)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const renderHTML = (target, data, start = 0, end=null) => {
|
|
62
|
+
let dataLen = data.length
|
|
63
|
+
|
|
64
|
+
let stop = end ? start + dataLen : dataLen - start
|
|
65
|
+
if (stop > dataLen) { stop = dataLen }
|
|
66
|
+
|
|
67
|
+
// Clear existing content
|
|
68
|
+
target.textContent = ''
|
|
69
|
+
const insertRow = appendRow.bind(target)
|
|
70
|
+
const parentElement = target.parentElement
|
|
71
|
+
const key = target[Config.KEY]
|
|
72
|
+
const childNodes = target.childNodes
|
|
73
|
+
const isTableSection = target.tagName === 'TBODY' || target.tagName === 'THEAD' || target.tagName === 'TFOOT'
|
|
74
|
+
if (!isTableSection) {
|
|
75
|
+
target.remove()
|
|
76
|
+
}
|
|
77
|
+
let fragment = globalThis.document.createDocumentFragment()
|
|
78
|
+
if (childNodes.length > 0) {
|
|
79
|
+
for (let i = start; i < stop; ++i) {
|
|
80
|
+
setNodeValues(target.processNode, data[i], target.dataSlots)
|
|
81
|
+
let cloned = cloneDeep(target.processNode)
|
|
82
|
+
cloned[Config.KEY] = getItemValue(data[i],key)
|
|
83
|
+
fragment.appendChild(cloned)
|
|
84
|
+
}
|
|
85
|
+
if (isTableSection) {
|
|
86
|
+
target.appendChild(fragment)
|
|
87
|
+
} else {
|
|
88
|
+
parentElement.appendChild(fragment)
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
for (let i = start; i < stop; ++i) {
|
|
92
|
+
setNodeValues(target.processNode, data[i], target.dataSlots)
|
|
93
|
+
let cloned = cloneDeep(target.processNode)
|
|
94
|
+
cloned[Config.KEY] = getItemValue(data[i],key)
|
|
95
|
+
insertRow(cloned)
|
|
96
|
+
}
|
|
97
|
+
if (!isTableSection) {
|
|
98
|
+
parentElement.appendChild(target)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const renderHTMLWithProvider = (target, dataProvider, start = 0, length = null, rows = []) => {
|
|
104
|
+
if (length === null || length === 0) {
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const stop = start + length
|
|
109
|
+
const key = target[Config.KEY]
|
|
110
|
+
const insertRow = appendRow.bind(target)
|
|
111
|
+
|
|
112
|
+
const table = target.parentElement
|
|
113
|
+
const wasAttached = table && table.contains(target)
|
|
114
|
+
|
|
115
|
+
if (wasAttached) {
|
|
116
|
+
target.remove()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
for (let i = start; i < stop; ++i) {
|
|
120
|
+
const dataItem = dataProvider(i, rows)
|
|
121
|
+
setNodeValues(target.processNode, dataItem, target.dataSlots)
|
|
122
|
+
let cloned = cloneDeep(target.processNode)
|
|
123
|
+
cloned[Config.KEY] = getItemValue(dataItem, key)
|
|
124
|
+
insertRow(cloned)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (wasAttached && table) {
|
|
128
|
+
table.append(target)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const defaultProvider = (i, list) => list[i]
|
|
133
|
+
|
|
134
|
+
const renderWithProvider = (target, dataList, start = 0, length = null, dataProvider = null) => {
|
|
135
|
+
const provider = dataProvider ?? defaultProvider
|
|
136
|
+
const len = length ?? (dataList?.length ?? 0) - start
|
|
137
|
+
if (len <= 0) return
|
|
138
|
+
renderHTMLWithProvider(target, provider, start, len, dataList ?? [])
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const add = (target, dataSet, start=0) => {
|
|
142
|
+
renderHTML(target, dataSet, start , dataSet.length - start)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const addWithProvider = (target, dataList, start = 0, length = null, dataProvider = null) => {
|
|
146
|
+
const provider = dataProvider ?? defaultProvider
|
|
147
|
+
const len = length ?? (dataList?.length ?? 0) - start
|
|
148
|
+
if (len <= 0) return
|
|
149
|
+
|
|
150
|
+
const stop = start + len
|
|
151
|
+
const key = target[Config.KEY]
|
|
152
|
+
const insertRow = appendRow.bind(target)
|
|
153
|
+
const rows = dataList ?? []
|
|
154
|
+
|
|
155
|
+
// Simple loop - append to existing content (keep attached)
|
|
156
|
+
for (let i = start; i < stop; ++i) {
|
|
157
|
+
// Call provider to get single data object for this index
|
|
158
|
+
const dataItem = provider(i, rows)
|
|
159
|
+
|
|
160
|
+
// Set values and clone the process node
|
|
161
|
+
setNodeValues(target.processNode, dataItem, target.dataSlots)
|
|
162
|
+
let cloned = cloneDeep(target.processNode)
|
|
163
|
+
cloned[Config.KEY] = getItemValue(dataItem, key)
|
|
164
|
+
insertRow(cloned)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @deprecated Use add() instead. Will be removed in a future version.
|
|
170
|
+
*/
|
|
171
|
+
const append = (target, dataSet, start = 0) => {
|
|
172
|
+
if (typeof globalThis !== 'undefined' && globalThis.console?.warn) {
|
|
173
|
+
globalThis.console.warn('DooHTML: append() is deprecated. Use add() instead.')
|
|
174
|
+
}
|
|
175
|
+
return add(target, dataSet, start)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* @deprecated Use addWithProvider() instead. Will be removed in a future version.
|
|
180
|
+
*/
|
|
181
|
+
const appendWithProvider = (target, dataList, start = 0, length = null, dataProvider = null) => {
|
|
182
|
+
if (typeof globalThis !== 'undefined' && globalThis.console?.warn) {
|
|
183
|
+
globalThis.console.warn('DooHTML: appendWithProvider() is deprecated. Use addWithProvider() instead.')
|
|
184
|
+
}
|
|
185
|
+
return addWithProvider(target, dataList, start, length, dataProvider)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const dooParse = (argDataNode) => {
|
|
189
|
+
const _xAttr = ['src', 'selected', 'checked', 'disabled', 'readonly']
|
|
190
|
+
|
|
191
|
+
let tplNode = cloneDeep(argDataNode)
|
|
192
|
+
tplNode.removeAttribute(Config.DATA_BIND)
|
|
193
|
+
delete tplNode.dataset.key
|
|
194
|
+
let htmlStr = tplNode.outerHTML.replaceAll('\t', '').replaceAll('\n', '')
|
|
195
|
+
let orgStr = htmlStr
|
|
196
|
+
_xAttr.forEach(item => {
|
|
197
|
+
htmlStr = htmlStr.replaceAll(new RegExp(' ' + item + '="{{(.+)}}"', 'g'), ' doo-' + item + '="{{$1}}"')
|
|
198
|
+
})
|
|
199
|
+
let xHtml = (orgStr === htmlStr)
|
|
200
|
+
|
|
201
|
+
let elem = globalThis.document.createElement('template')
|
|
202
|
+
elem.innerHTML = htmlStr
|
|
203
|
+
let dataSlots = []
|
|
204
|
+
|
|
205
|
+
const addDataSlot = (item, fld, type) => {
|
|
206
|
+
let slot = []
|
|
207
|
+
let child = item
|
|
208
|
+
while (child !== elem.firstElementChild) {
|
|
209
|
+
let prev = child.previousSibling
|
|
210
|
+
let cnt = 0
|
|
211
|
+
|
|
212
|
+
while (prev) {
|
|
213
|
+
cnt++
|
|
214
|
+
prev = prev.previousSibling
|
|
215
|
+
}
|
|
216
|
+
child = child.parentNode
|
|
217
|
+
if (child) {
|
|
218
|
+
slot.unshift(cnt)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
dataSlots.push([fld,slot.slice(1),type])
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const textWalker = globalThis.document.createTreeWalker(
|
|
225
|
+
elem.content,
|
|
226
|
+
globalThis.NodeFilter.SHOW_TEXT,
|
|
227
|
+
{
|
|
228
|
+
acceptNode() {
|
|
229
|
+
return globalThis.NodeFilter.FILTER_ACCEPT
|
|
230
|
+
},
|
|
231
|
+
}
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
let textNode = textWalker.nextNode()
|
|
235
|
+
let multiText = []
|
|
236
|
+
while (textNode) {
|
|
237
|
+
let val = textNode.wholeText.trim()
|
|
238
|
+
if (val.indexOf('{{') === 0 && val.lastIndexOf('}}') === val.length-2) {
|
|
239
|
+
//do nothing
|
|
240
|
+
} else {
|
|
241
|
+
let text = val.replaceAll('{{', '<span>{{').replaceAll('}}', '}}</span>')
|
|
242
|
+
multiText.push({node:textNode.parentNode, oldText:val, newText:text})
|
|
243
|
+
|
|
244
|
+
}
|
|
245
|
+
textNode = textWalker.nextNode()
|
|
246
|
+
}
|
|
247
|
+
for (let i=0, len = multiText.length; i<len; i++) {
|
|
248
|
+
multiText[i].node.innerHTML = multiText[i].node.innerHTML.replace(multiText[i].oldText,multiText[i].newText)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let processedElem = cloneDeep(elem.content)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
const treeWalker = globalThis.document.createTreeWalker(
|
|
255
|
+
processedElem,
|
|
256
|
+
globalThis.NodeFilter.SHOW_TEXT,
|
|
257
|
+
{
|
|
258
|
+
acceptNode() {
|
|
259
|
+
return globalThis.NodeFilter.FILTER_ACCEPT
|
|
260
|
+
},
|
|
261
|
+
}
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
let currentNode = treeWalker.nextNode()
|
|
265
|
+
while (currentNode) {
|
|
266
|
+
const matches = currentNode.nodeValue.match(/\{\{(.*?)\}\}/g)
|
|
267
|
+
if (matches) {
|
|
268
|
+
const parent = currentNode.parentNode
|
|
269
|
+
matches.forEach((match) => {
|
|
270
|
+
const fld = match.replaceAll(/\{\{|\}\}/g, '').trim()
|
|
271
|
+
const textNode = globalThis.document.createTextNode(fld)
|
|
272
|
+
currentNode.textContent = ''
|
|
273
|
+
const newNode = parent.appendChild(textNode)
|
|
274
|
+
addDataSlot(newNode, fld, 'textContent')
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
currentNode = treeWalker.nextNode()
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const elemWalker = globalThis.document.createTreeWalker(
|
|
282
|
+
processedElem,
|
|
283
|
+
globalThis.NodeFilter.SHOW_ELEMENT,
|
|
284
|
+
{
|
|
285
|
+
acceptNode() {
|
|
286
|
+
return globalThis.NodeFilter.FILTER_ACCEPT
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
currentNode = elemWalker.nextNode()
|
|
292
|
+
while (currentNode) {
|
|
293
|
+
for (const attr of currentNode.attributes) {
|
|
294
|
+
if (attr.nodeValue.includes('{{')) {
|
|
295
|
+
addDataSlot(currentNode, attr.nodeValue.replace('{{','').replace('}}',''), attr.name)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
currentNode = elemWalker.nextNode()
|
|
299
|
+
}
|
|
300
|
+
let templateStr = processedElem.firstElementChild.outerHTML
|
|
301
|
+
dataSlots.forEach(item=>{
|
|
302
|
+
let str = '{{' + item[0] + '}}'
|
|
303
|
+
templateStr = templateStr.replaceAll(new RegExp(str,'g'),'')
|
|
304
|
+
|
|
305
|
+
})
|
|
306
|
+
processedElem.outerHTML = templateStr
|
|
307
|
+
|
|
308
|
+
return {processNode:processedElem.firstElementChild, xHtml, dataSlots}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const fetchTemplate = (url) => {
|
|
312
|
+
return new Promise((resolve, reject) => {
|
|
313
|
+
const xhr = new XMLHttpRequest()
|
|
314
|
+
xhr.open("GET", url)
|
|
315
|
+
xhr.addEventListener('load', () => resolve(xhr.responseText))
|
|
316
|
+
xhr.onerror = () => reject(xhr.statusText)
|
|
317
|
+
xhr.send()
|
|
318
|
+
})
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const setReactiveDataNodes = (tplNode) => {
|
|
322
|
+
const place = []
|
|
323
|
+
const getNodeLevel = (node) => {
|
|
324
|
+
let level = 0
|
|
325
|
+
while (node.parentElement) {
|
|
326
|
+
node = node.parentElement
|
|
327
|
+
level++
|
|
328
|
+
}
|
|
329
|
+
return level
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const processReactiveElements = (reactiveElems) => {
|
|
333
|
+
const orderedElems = [...reactiveElems]
|
|
334
|
+
.map((elem) => ({
|
|
335
|
+
elem,
|
|
336
|
+
level: getNodeLevel(elem),
|
|
337
|
+
useParent: elem.dataset.src?.startsWith('this.parent'),
|
|
338
|
+
noRepeat: Object.hasOwn(elem.dataset, 'norepeat'),
|
|
339
|
+
}))
|
|
340
|
+
.sort((a, b) => b.level - a.level)
|
|
341
|
+
|
|
342
|
+
orderedElems.forEach(({ elem, level, useParent, noRepeat }, index) => {
|
|
343
|
+
const parent = elem.parentElement
|
|
344
|
+
// Keep TBODY/THEAD/TFOOT as container so TABLE keeps thead when data-bind is on tbody
|
|
345
|
+
const isTableSection = parent?.tagName === 'TABLE' && '|TBODY|THEAD|TFOOT|'.includes(`|${elem.tagName}|`)
|
|
346
|
+
let dataElem = isTableSection
|
|
347
|
+
? elem
|
|
348
|
+
: '|STYLE|LINK|'.includes(`|${elem.tagName}|`)
|
|
349
|
+
? elem
|
|
350
|
+
: parent && '|DL|UL|TBODY|THEAD|TFOOT|TR|SELECT|SECTION|'.includes(`|${parent.tagName}|`)
|
|
351
|
+
? parent
|
|
352
|
+
: parent || elem // Fallback to elem if parentElement is null
|
|
353
|
+
|
|
354
|
+
// Ensure dataElem is never null
|
|
355
|
+
if (!dataElem) {
|
|
356
|
+
console.warn('dataElem is null, using elem as fallback')
|
|
357
|
+
dataElem = elem
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const parsedNode = dooParse(elem)
|
|
361
|
+
Object.assign(dataElem, {
|
|
362
|
+
processNode: parsedNode.processNode,
|
|
363
|
+
xHtml: parsedNode.xHtml,
|
|
364
|
+
dataSlots: parsedNode.dataSlots,
|
|
365
|
+
name: index,
|
|
366
|
+
level,
|
|
367
|
+
useParent,
|
|
368
|
+
noRepeat,
|
|
369
|
+
isTable: isTable(dataElem),
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
if (dataElem.tagName === 'DATA' || dataElem.tagName === 'STYLE' || dataElem.tagName === 'LINK') {
|
|
373
|
+
elem.parentElement?.replaceChild(dataElem, elem) || console.warn('Templates should only have one child node')
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
place.push(dataElem)
|
|
377
|
+
})
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const reactiveElems = tplNode.content.querySelectorAll(`[${Config.DATA_BIND}]`)
|
|
381
|
+
reactiveElems.forEach((elem) => {
|
|
382
|
+
if (!Object.hasOwn(elem.dataset, 'src')) {
|
|
383
|
+
elem.dataset.src = tplNode.hasAttribute('doo-dispatch') ? 'DooX' : Config.DATA_BIND
|
|
384
|
+
}
|
|
385
|
+
delete elem.dataset.src
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
processReactiveElements(reactiveElems)
|
|
389
|
+
|
|
390
|
+
tplNode.place = place
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
const prefetchTemplate = async (src) => {
|
|
395
|
+
if (src && (src.startsWith('./') || src.startsWith('../') || src.startsWith('http'))) {
|
|
396
|
+
const tpl = await fetchTemplate(src)
|
|
397
|
+
return tpl
|
|
398
|
+
}
|
|
399
|
+
return null
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const createTemplate = async (id, data = [], src = null) => {
|
|
403
|
+
|
|
404
|
+
let tpl = src ? await prefetchTemplate(src) : ''
|
|
405
|
+
if (!tpl) {
|
|
406
|
+
if (id.startsWith('<')) {
|
|
407
|
+
tpl = id
|
|
408
|
+
} else if (id.startsWith('#')) {
|
|
409
|
+
tpl = globalThis.document.querySelector(id).outerHTML
|
|
410
|
+
} else {
|
|
411
|
+
tpl = globalThis.document.querySelector('#' + id).outerHTML
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const elem = globalThis.document.createElement('div')
|
|
416
|
+
elem.innerHTML = tpl
|
|
417
|
+
if (elem.querySelector('template')) {
|
|
418
|
+
elem.innerHTML = elem.querySelector('template')
|
|
419
|
+
? tpl
|
|
420
|
+
: `<template><center><pre>The template you are trying to import does not have a <template> tag</pre><div style="color:red">${tpl}</div></center></template>`
|
|
421
|
+
}
|
|
422
|
+
const importedTemplate = cloneDeep(elem.querySelector('template'))
|
|
423
|
+
|
|
424
|
+
const templateNode = globalThis.document.createElement('template')
|
|
425
|
+
importedTemplate.removeAttribute('id')
|
|
426
|
+
templateNode.innerHTML = importedTemplate.innerHTML
|
|
427
|
+
|
|
428
|
+
setReactiveDataNodes(templateNode)
|
|
429
|
+
const subscriber = globalThis.document.querySelector(`[data-template="${id}"]`)
|
|
430
|
+
templateNode["place"][0].textContent = ''
|
|
431
|
+
templateNode["place"][0][Config.KEY] = subscriber.dataset[Config.KEY]
|
|
432
|
+
subscriber.parentElement.replaceChild(templateNode.content, subscriber)
|
|
433
|
+
|
|
434
|
+
if (data.length > 0) {
|
|
435
|
+
render(templateNode["place"][0], data, 0)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return templateNode["place"][0]
|
|
439
|
+
}
|
|
440
|
+
export { createTemplate, add, addWithProvider, append, appendWithProvider, render, renderWithProvider, Config, version, prefetchTemplate }
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "doohtml",
|
|
3
|
+
"version": "0.98.9-beta.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "DooHTML is a JavaScript library for creating HTML elements and templates using high performant rendering methods",
|
|
6
|
+
"main": "dist/doohtml.js",
|
|
7
|
+
"module": "dist/doohtml.mjs",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/doohtml.mjs",
|
|
11
|
+
"require": "./dist/doohtml.js",
|
|
12
|
+
"default": "./dist/doohtml.mjs"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "npm run build:copy && npm run build:min",
|
|
21
|
+
"build:copy": "mkdir -p dist && cp src/doohtml.js dist/doohtml.js && cp src/doohtml.js dist/doohtml.mjs",
|
|
22
|
+
"build:min": "terser src/doohtml.js -c --mangle --mangle-props keep_quoted -o dist/doohtml.min.mjs",
|
|
23
|
+
"preview": "npm run build && npm pack",
|
|
24
|
+
"prepublishOnly": "npm run build",
|
|
25
|
+
"test": "node test/doohtml.test.js",
|
|
26
|
+
"serve": "serve . -l 4000"
|
|
27
|
+
},
|
|
28
|
+
"author": "Henrik Javen",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/hman61/doohtml.git"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"html",
|
|
36
|
+
"template",
|
|
37
|
+
"rendering",
|
|
38
|
+
"dom",
|
|
39
|
+
"framework"
|
|
40
|
+
],
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"eslint": "^9.39.2",
|
|
43
|
+
"eslint-plugin-unicorn": "^62.0.0",
|
|
44
|
+
"jsdom": "^25.0.0",
|
|
45
|
+
"serve": "^14.2.5",
|
|
46
|
+
"terser": "^5.20.2"
|
|
47
|
+
}
|
|
48
|
+
}
|