neiki-editor 2.9.2 → 2.9.3
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 +22 -11
- package/dist/neiki-editor.css +1 -1
- package/dist/neiki-editor.js +292 -90
- package/dist/neiki-editor.min.js +1 -1
- package/package.json +1 -1
package/dist/neiki-editor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* NeikiEditor - A Modern WYSIWYG Editor
|
|
3
|
-
* Version: 2.9.
|
|
3
|
+
* Version: 2.9.3
|
|
4
4
|
*
|
|
5
5
|
* A lightweight, feature-rich text editor with support for:
|
|
6
6
|
* - Rich text formatting (bold, italic, underline, etc.)
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
// SECTION 1: CONFIGURATION & CONSTANTS
|
|
20
20
|
// ============================================
|
|
21
21
|
|
|
22
|
+
let EDITOR_INSTANCE_COUNTER = 0;
|
|
23
|
+
|
|
22
24
|
// ============================================
|
|
23
25
|
// TRANSLATIONS / i18n
|
|
24
26
|
// ============================================
|
|
@@ -1089,8 +1091,9 @@
|
|
|
1089
1091
|
// Register a custom translation (static method — available before init)
|
|
1090
1092
|
function addTranslation(lang, keys) {
|
|
1091
1093
|
if (!lang || typeof keys !== 'object') return;
|
|
1094
|
+
if (!Utils.isSafeObjectKey(lang)) return;
|
|
1092
1095
|
if (!TRANSLATIONS[lang]) TRANSLATIONS[lang] = {};
|
|
1093
|
-
|
|
1096
|
+
Utils.safeAssign(TRANSLATIONS[lang], keys);
|
|
1094
1097
|
}
|
|
1095
1098
|
|
|
1096
1099
|
// Translation helper function
|
|
@@ -1127,6 +1130,7 @@
|
|
|
1127
1130
|
theme: 'light',
|
|
1128
1131
|
language: 'en',
|
|
1129
1132
|
translations: null,
|
|
1133
|
+
autosaveKey: null,
|
|
1130
1134
|
plugins: [],
|
|
1131
1135
|
onChange: null,
|
|
1132
1136
|
onSave: null,
|
|
@@ -1284,8 +1288,12 @@
|
|
|
1284
1288
|
el.textContent = value;
|
|
1285
1289
|
} else if (key.startsWith('on') && typeof value === 'function') {
|
|
1286
1290
|
el.addEventListener(key.slice(2).toLowerCase(), value);
|
|
1287
|
-
} else if (key === 'style' && typeof value === 'object') {
|
|
1288
|
-
Object.
|
|
1291
|
+
} else if (key === 'style' && value && typeof value === 'object') {
|
|
1292
|
+
Object.keys(value).forEach(styleName => {
|
|
1293
|
+
if (Utils.isSafeObjectKey(styleName)) {
|
|
1294
|
+
el.style[styleName] = value[styleName];
|
|
1295
|
+
}
|
|
1296
|
+
});
|
|
1289
1297
|
} else {
|
|
1290
1298
|
el.setAttribute(key, value);
|
|
1291
1299
|
}
|
|
@@ -1308,9 +1316,24 @@
|
|
|
1308
1316
|
};
|
|
1309
1317
|
},
|
|
1310
1318
|
|
|
1319
|
+
isSafeObjectKey(key) {
|
|
1320
|
+
return key !== '__proto__' && key !== 'prototype' && key !== 'constructor';
|
|
1321
|
+
},
|
|
1322
|
+
|
|
1323
|
+
safeAssign(target, source) {
|
|
1324
|
+
if (!source || typeof source !== 'object') return target;
|
|
1325
|
+
Object.keys(source).forEach(key => {
|
|
1326
|
+
if (!Object.prototype.hasOwnProperty.call(source, key) || !Utils.isSafeObjectKey(key)) return;
|
|
1327
|
+
target[key] = source[key];
|
|
1328
|
+
});
|
|
1329
|
+
return target;
|
|
1330
|
+
},
|
|
1331
|
+
|
|
1311
1332
|
deepMerge(target, source) {
|
|
1312
1333
|
const result = { ...target };
|
|
1334
|
+
if (!source || typeof source !== 'object') return result;
|
|
1313
1335
|
Object.keys(source).forEach(key => {
|
|
1336
|
+
if (!Object.prototype.hasOwnProperty.call(source, key) || !Utils.isSafeObjectKey(key)) return;
|
|
1314
1337
|
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
1315
1338
|
result[key] = Utils.deepMerge(result[key] || {}, source[key]);
|
|
1316
1339
|
} else {
|
|
@@ -1321,11 +1344,64 @@
|
|
|
1321
1344
|
},
|
|
1322
1345
|
|
|
1323
1346
|
sanitizeHTML(html) {
|
|
1324
|
-
const
|
|
1325
|
-
|
|
1326
|
-
const
|
|
1327
|
-
|
|
1328
|
-
|
|
1347
|
+
const parser = new DOMParser();
|
|
1348
|
+
const doc = parser.parseFromString(String(html || ''), 'text/html');
|
|
1349
|
+
const blockedTags = new Set(['script', 'style', 'iframe', 'object', 'embed', 'link', 'meta', 'base']);
|
|
1350
|
+
const urlAttrs = new Set(['href', 'src', 'xlink:href', 'poster']);
|
|
1351
|
+
|
|
1352
|
+
doc.body.querySelectorAll('*').forEach(el => {
|
|
1353
|
+
if (blockedTags.has(el.tagName.toLowerCase())) {
|
|
1354
|
+
el.remove();
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
Array.from(el.attributes).forEach(attr => {
|
|
1359
|
+
const name = attr.name.toLowerCase();
|
|
1360
|
+
const value = attr.value.trim();
|
|
1361
|
+
|
|
1362
|
+
if (name.startsWith('on') || name === 'srcdoc') {
|
|
1363
|
+
el.removeAttribute(attr.name);
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
if (urlAttrs.has(name) && !Utils.isSafeUrl(value, el.tagName.toLowerCase() === 'img')) {
|
|
1368
|
+
el.removeAttribute(attr.name);
|
|
1369
|
+
}
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1372
|
+
if (el.tagName.toLowerCase() === 'a' && el.getAttribute('target') === '_blank') {
|
|
1373
|
+
el.setAttribute('rel', 'noopener noreferrer');
|
|
1374
|
+
}
|
|
1375
|
+
});
|
|
1376
|
+
|
|
1377
|
+
return doc.body.innerHTML;
|
|
1378
|
+
},
|
|
1379
|
+
|
|
1380
|
+
isSafeUrl(value, allowImageData = false) {
|
|
1381
|
+
if (!value) return true;
|
|
1382
|
+
if (value.startsWith('#') || value.startsWith('/') || value.startsWith('./') || value.startsWith('../')) return true;
|
|
1383
|
+
|
|
1384
|
+
try {
|
|
1385
|
+
const parsed = new URL(value, window.location.href);
|
|
1386
|
+
const protocol = parsed.protocol.toLowerCase();
|
|
1387
|
+
return protocol === 'http:' ||
|
|
1388
|
+
protocol === 'https:' ||
|
|
1389
|
+
protocol === 'mailto:' ||
|
|
1390
|
+
protocol === 'tel:' ||
|
|
1391
|
+
(allowImageData && protocol === 'data:' && /^data:image\//i.test(value));
|
|
1392
|
+
} catch (e) {
|
|
1393
|
+
return false;
|
|
1394
|
+
}
|
|
1395
|
+
},
|
|
1396
|
+
|
|
1397
|
+
escapeHTML(value) {
|
|
1398
|
+
return String(value ?? '').replace(/[&<>"']/g, char => ({
|
|
1399
|
+
'&': '&',
|
|
1400
|
+
'<': '<',
|
|
1401
|
+
'>': '>',
|
|
1402
|
+
'"': '"',
|
|
1403
|
+
"'": '''
|
|
1404
|
+
})[char]);
|
|
1329
1405
|
},
|
|
1330
1406
|
|
|
1331
1407
|
isValidUrl(string) {
|
|
@@ -1685,38 +1761,74 @@
|
|
|
1685
1761
|
createLinkModal(data) {
|
|
1686
1762
|
const modal = Utils.createElement('div', { className: 'neiki-modal' });
|
|
1687
1763
|
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
<input type="url" class="neiki-input" name="url" placeholder="https://example.com" value="${data.url || ''}">
|
|
1697
|
-
</div>
|
|
1698
|
-
<div class="neiki-form-group">
|
|
1699
|
-
<label>${t('modal.text')}</label>
|
|
1700
|
-
<input type="text" class="neiki-input" name="text" placeholder="${t('modal.linkText')}" value="${data.text || ''}">
|
|
1701
|
-
</div>
|
|
1702
|
-
<div class="neiki-form-group">
|
|
1703
|
-
<label>
|
|
1704
|
-
<input type="checkbox" name="newTab" ${data.newTab ? 'checked' : ''}> ${t('modal.openInNewTab')}
|
|
1705
|
-
</label>
|
|
1706
|
-
</div>
|
|
1707
|
-
</div>
|
|
1708
|
-
<div class="neiki-modal-footer">
|
|
1709
|
-
<button class="neiki-btn neiki-btn-secondary" type="button" data-action="cancel">${t('modal.cancel')}</button>
|
|
1710
|
-
<button class="neiki-btn neiki-btn-primary" type="button" data-action="insert">${t('modal.insert')}</button>
|
|
1711
|
-
</div>
|
|
1712
|
-
`;
|
|
1764
|
+
const header = Utils.createElement('div', { className: 'neiki-modal-header' });
|
|
1765
|
+
header.appendChild(Utils.createElement('h3', { textContent: t('modal.insertLink') }));
|
|
1766
|
+
const closeBtn = Utils.createElement('button', {
|
|
1767
|
+
className: 'neiki-modal-close',
|
|
1768
|
+
type: 'button',
|
|
1769
|
+
innerHTML: Icons.close
|
|
1770
|
+
});
|
|
1771
|
+
header.appendChild(closeBtn);
|
|
1713
1772
|
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1773
|
+
const body = Utils.createElement('div', { className: 'neiki-modal-body' });
|
|
1774
|
+
const urlGroup = Utils.createElement('div', { className: 'neiki-form-group' });
|
|
1775
|
+
const urlInput = Utils.createElement('input', {
|
|
1776
|
+
type: 'url',
|
|
1777
|
+
className: 'neiki-input',
|
|
1778
|
+
name: 'url',
|
|
1779
|
+
placeholder: 'https://example.com'
|
|
1780
|
+
});
|
|
1781
|
+
urlInput.value = data.url || '';
|
|
1782
|
+
urlGroup.appendChild(Utils.createElement('label', { textContent: t('modal.url') }));
|
|
1783
|
+
urlGroup.appendChild(urlInput);
|
|
1784
|
+
|
|
1785
|
+
const textGroup = Utils.createElement('div', { className: 'neiki-form-group' });
|
|
1786
|
+
const textInput = Utils.createElement('input', {
|
|
1787
|
+
type: 'text',
|
|
1788
|
+
className: 'neiki-input',
|
|
1789
|
+
name: 'text',
|
|
1790
|
+
placeholder: t('modal.linkText')
|
|
1791
|
+
});
|
|
1792
|
+
textInput.value = data.text || '';
|
|
1793
|
+
textGroup.appendChild(Utils.createElement('label', { textContent: t('modal.text') }));
|
|
1794
|
+
textGroup.appendChild(textInput);
|
|
1795
|
+
|
|
1796
|
+
const newTabGroup = Utils.createElement('div', { className: 'neiki-form-group' });
|
|
1797
|
+
const newTabLabel = Utils.createElement('label');
|
|
1798
|
+
const newTabInput = Utils.createElement('input', { type: 'checkbox', name: 'newTab' });
|
|
1799
|
+
newTabInput.checked = !!data.newTab;
|
|
1800
|
+
newTabLabel.appendChild(newTabInput);
|
|
1801
|
+
newTabLabel.appendChild(document.createTextNode(' ' + t('modal.openInNewTab')));
|
|
1802
|
+
newTabGroup.appendChild(newTabLabel);
|
|
1803
|
+
|
|
1804
|
+
body.appendChild(urlGroup);
|
|
1805
|
+
body.appendChild(textGroup);
|
|
1806
|
+
body.appendChild(newTabGroup);
|
|
1807
|
+
|
|
1808
|
+
const footer = Utils.createElement('div', { className: 'neiki-modal-footer' });
|
|
1809
|
+
const cancelBtn = Utils.createElement('button', {
|
|
1810
|
+
className: 'neiki-btn neiki-btn-secondary',
|
|
1811
|
+
type: 'button',
|
|
1812
|
+
textContent: t('modal.cancel')
|
|
1813
|
+
});
|
|
1814
|
+
const insertBtn = Utils.createElement('button', {
|
|
1815
|
+
className: 'neiki-btn neiki-btn-primary',
|
|
1816
|
+
type: 'button',
|
|
1817
|
+
textContent: t('modal.insert')
|
|
1818
|
+
});
|
|
1819
|
+
footer.appendChild(cancelBtn);
|
|
1820
|
+
footer.appendChild(insertBtn);
|
|
1821
|
+
|
|
1822
|
+
modal.appendChild(header);
|
|
1823
|
+
modal.appendChild(body);
|
|
1824
|
+
modal.appendChild(footer);
|
|
1825
|
+
|
|
1826
|
+
closeBtn.addEventListener('click', () => this.close());
|
|
1827
|
+
cancelBtn.addEventListener('click', () => this.close());
|
|
1828
|
+
insertBtn.addEventListener('click', () => {
|
|
1829
|
+
const url = urlInput.value;
|
|
1830
|
+
const text = textInput.value || url;
|
|
1831
|
+
const newTab = newTabInput.checked;
|
|
1720
1832
|
|
|
1721
1833
|
if (url) {
|
|
1722
1834
|
this.editor.restoreSavedSelection();
|
|
@@ -1735,43 +1847,47 @@
|
|
|
1735
1847
|
|
|
1736
1848
|
modal.innerHTML = `
|
|
1737
1849
|
<div class="neiki-modal-header">
|
|
1738
|
-
<h3>${t('modal.insertImage')}</h3>
|
|
1850
|
+
<h3>${Utils.escapeHTML(t('modal.insertImage'))}</h3>
|
|
1739
1851
|
<button class="neiki-modal-close" type="button">${Icons.close}</button>
|
|
1740
1852
|
</div>
|
|
1741
1853
|
<div class="neiki-modal-body">
|
|
1742
1854
|
<div class="neiki-form-group">
|
|
1743
|
-
<label>${t('modal.uploadImage')}</label>
|
|
1855
|
+
<label>${Utils.escapeHTML(t('modal.uploadImage'))}</label>
|
|
1744
1856
|
<input type="file" class="neiki-input" name="upload" accept="image/*" multiple>
|
|
1745
|
-
<small class="neiki-upload-hint" style="color: var(--neiki-text-muted); font-size: 11px;">${uploadHint}</small>
|
|
1857
|
+
<small class="neiki-upload-hint" style="color: var(--neiki-text-muted); font-size: 11px;">${Utils.escapeHTML(uploadHint)}</small>
|
|
1746
1858
|
</div>
|
|
1747
1859
|
<div class="neiki-form-divider">
|
|
1748
|
-
<span>${t('modal.or')}</span>
|
|
1860
|
+
<span>${Utils.escapeHTML(t('modal.or'))}</span>
|
|
1749
1861
|
</div>
|
|
1750
1862
|
<div class="neiki-form-group">
|
|
1751
|
-
<label>${t('modal.imageUrl')}</label>
|
|
1752
|
-
<input type="url" class="neiki-input" name="url" placeholder="https://example.com/image.jpg"
|
|
1863
|
+
<label>${Utils.escapeHTML(t('modal.imageUrl'))}</label>
|
|
1864
|
+
<input type="url" class="neiki-input" name="url" placeholder="https://example.com/image.jpg">
|
|
1753
1865
|
</div>
|
|
1754
1866
|
<div class="neiki-form-group">
|
|
1755
|
-
<label>${t('modal.altText')}</label>
|
|
1756
|
-
<input type="text" class="neiki-input" name="alt"
|
|
1867
|
+
<label>${Utils.escapeHTML(t('modal.altText'))}</label>
|
|
1868
|
+
<input type="text" class="neiki-input" name="alt">
|
|
1757
1869
|
</div>
|
|
1758
1870
|
<div class="neiki-form-group">
|
|
1759
|
-
<label>${t('modal.widthOptional')}</label>
|
|
1760
|
-
<input type="text" class="neiki-input" name="width" placeholder="e.g. 300px or 50%"
|
|
1871
|
+
<label>${Utils.escapeHTML(t('modal.widthOptional'))}</label>
|
|
1872
|
+
<input type="text" class="neiki-input" name="width" placeholder="e.g. 300px or 50%">
|
|
1761
1873
|
</div>
|
|
1762
1874
|
</div>
|
|
1763
1875
|
<div class="neiki-modal-footer">
|
|
1764
|
-
<button class="neiki-btn neiki-btn-secondary" type="button" data-action="cancel">${t('modal.cancel')}</button>
|
|
1765
|
-
<button class="neiki-btn neiki-btn-primary" type="button" data-action="insert">${t('modal.insert')}</button>
|
|
1876
|
+
<button class="neiki-btn neiki-btn-secondary" type="button" data-action="cancel">${Utils.escapeHTML(t('modal.cancel'))}</button>
|
|
1877
|
+
<button class="neiki-btn neiki-btn-primary" type="button" data-action="insert">${Utils.escapeHTML(t('modal.insert'))}</button>
|
|
1766
1878
|
</div>
|
|
1767
1879
|
`;
|
|
1768
1880
|
|
|
1769
1881
|
const uploadInput = modal.querySelector('[name="upload"]');
|
|
1770
1882
|
const urlInput = modal.querySelector('[name="url"]');
|
|
1771
|
-
const hintEl = modal.querySelector('.neiki-upload-hint');
|
|
1772
1883
|
const insertBtn = modal.querySelector('[data-action="insert"]');
|
|
1773
1884
|
let pendingFiles = [];
|
|
1774
1885
|
|
|
1886
|
+
urlInput.value = data.url || '';
|
|
1887
|
+
modal.querySelector('[name="alt"]').placeholder = t('modal.describeImage');
|
|
1888
|
+
modal.querySelector('[name="alt"]').value = data.alt || '';
|
|
1889
|
+
modal.querySelector('[name="width"]').value = data.width || '';
|
|
1890
|
+
|
|
1775
1891
|
// Handle file upload (supports multiple files)
|
|
1776
1892
|
uploadInput.addEventListener('change', (e) => {
|
|
1777
1893
|
const files = Array.from(e.target.files).filter(f => f.type.startsWith('image/'));
|
|
@@ -1842,7 +1958,6 @@
|
|
|
1842
1958
|
this.close();
|
|
1843
1959
|
} else if (pendingFiles.length > 1) {
|
|
1844
1960
|
// Multiple files without handler — insert each as base64
|
|
1845
|
-
let loaded = 0;
|
|
1846
1961
|
insertBtn.disabled = true;
|
|
1847
1962
|
insertBtn.textContent = t('modal.uploadingImage');
|
|
1848
1963
|
|
|
@@ -1878,32 +1993,35 @@
|
|
|
1878
1993
|
|
|
1879
1994
|
modal.innerHTML = `
|
|
1880
1995
|
<div class="neiki-modal-header">
|
|
1881
|
-
<h3>${t('modal.insertTable')}</h3>
|
|
1996
|
+
<h3>${Utils.escapeHTML(t('modal.insertTable'))}</h3>
|
|
1882
1997
|
<button class="neiki-modal-close" type="button">${Icons.close}</button>
|
|
1883
1998
|
</div>
|
|
1884
1999
|
<div class="neiki-modal-body">
|
|
1885
2000
|
<div class="neiki-form-row">
|
|
1886
2001
|
<div class="neiki-form-group">
|
|
1887
|
-
<label>${t('modal.rows')}</label>
|
|
1888
|
-
<input type="number" class="neiki-input" name="rows" min="1" max="20"
|
|
2002
|
+
<label>${Utils.escapeHTML(t('modal.rows'))}</label>
|
|
2003
|
+
<input type="number" class="neiki-input" name="rows" min="1" max="20">
|
|
1889
2004
|
</div>
|
|
1890
2005
|
<div class="neiki-form-group">
|
|
1891
|
-
<label>${t('modal.columns')}</label>
|
|
1892
|
-
<input type="number" class="neiki-input" name="cols" min="1" max="10"
|
|
2006
|
+
<label>${Utils.escapeHTML(t('modal.columns'))}</label>
|
|
2007
|
+
<input type="number" class="neiki-input" name="cols" min="1" max="10">
|
|
1893
2008
|
</div>
|
|
1894
2009
|
</div>
|
|
1895
2010
|
<div class="neiki-form-group">
|
|
1896
2011
|
<label>
|
|
1897
|
-
<input type="checkbox" name="header" ${
|
|
2012
|
+
<input type="checkbox" name="header"> ${Utils.escapeHTML(t('modal.includeHeaderRow'))}
|
|
1898
2013
|
</label>
|
|
1899
2014
|
</div>
|
|
1900
2015
|
</div>
|
|
1901
2016
|
<div class="neiki-modal-footer">
|
|
1902
|
-
<button class="neiki-btn neiki-btn-secondary" type="button" data-action="cancel">${t('modal.cancel')}</button>
|
|
1903
|
-
<button class="neiki-btn neiki-btn-primary" type="button" data-action="insert">${t('modal.insert')}</button>
|
|
2017
|
+
<button class="neiki-btn neiki-btn-secondary" type="button" data-action="cancel">${Utils.escapeHTML(t('modal.cancel'))}</button>
|
|
2018
|
+
<button class="neiki-btn neiki-btn-primary" type="button" data-action="insert">${Utils.escapeHTML(t('modal.insert'))}</button>
|
|
1904
2019
|
</div>
|
|
1905
2020
|
`;
|
|
1906
2021
|
|
|
2022
|
+
modal.querySelector('[name="rows"]').value = parseInt(data.rows, 10) || 3;
|
|
2023
|
+
modal.querySelector('[name="cols"]').value = parseInt(data.cols, 10) || 3;
|
|
2024
|
+
modal.querySelector('[name="header"]').checked = data.header !== false;
|
|
1907
2025
|
modal.querySelector('.neiki-modal-close').addEventListener('click', () => this.close());
|
|
1908
2026
|
modal.querySelector('[data-action="cancel"]').addEventListener('click', () => this.close());
|
|
1909
2027
|
modal.querySelector('[data-action="insert"]').addEventListener('click', () => {
|
|
@@ -1924,28 +2042,28 @@
|
|
|
1924
2042
|
|
|
1925
2043
|
modal.innerHTML = `
|
|
1926
2044
|
<div class="neiki-modal-header">
|
|
1927
|
-
<h3>${t('modal.findReplace')}</h3>
|
|
2045
|
+
<h3>${Utils.escapeHTML(t('modal.findReplace'))}</h3>
|
|
1928
2046
|
<button class="neiki-modal-close" type="button">${Icons.close}</button>
|
|
1929
2047
|
</div>
|
|
1930
2048
|
<div class="neiki-modal-body">
|
|
1931
2049
|
<div class="neiki-form-group">
|
|
1932
|
-
<label>${t('modal.find')}</label>
|
|
1933
|
-
<input type="text" class="neiki-input" name="find"
|
|
2050
|
+
<label>${Utils.escapeHTML(t('modal.find'))}</label>
|
|
2051
|
+
<input type="text" class="neiki-input" name="find">
|
|
1934
2052
|
</div>
|
|
1935
2053
|
<div class="neiki-form-group">
|
|
1936
|
-
<label>${t('modal.replaceWith')}</label>
|
|
1937
|
-
<input type="text" class="neiki-input" name="replace"
|
|
2054
|
+
<label>${Utils.escapeHTML(t('modal.replaceWith'))}</label>
|
|
2055
|
+
<input type="text" class="neiki-input" name="replace">
|
|
1938
2056
|
</div>
|
|
1939
2057
|
<div class="neiki-form-group neiki-form-row">
|
|
1940
|
-
<label><input type="checkbox" name="regex"> ${t('modal.useRegex')}</label>
|
|
1941
|
-
<label><input type="checkbox" name="caseSensitive"> ${t('modal.caseSensitive')}</label>
|
|
2058
|
+
<label><input type="checkbox" name="regex"> ${Utils.escapeHTML(t('modal.useRegex'))}</label>
|
|
2059
|
+
<label><input type="checkbox" name="caseSensitive"> ${Utils.escapeHTML(t('modal.caseSensitive'))}</label>
|
|
1942
2060
|
</div>
|
|
1943
2061
|
<div class="neiki-find-results" style="margin-top:10px;font-size:13px;color:var(--neiki-text-muted);"></div>
|
|
1944
2062
|
</div>
|
|
1945
2063
|
<div class="neiki-modal-footer">
|
|
1946
|
-
<button class="neiki-btn neiki-btn-secondary" type="button" data-action="findNext">${t('modal.findNext')}</button>
|
|
1947
|
-
<button class="neiki-btn neiki-btn-secondary" type="button" data-action="replaceOne">${t('modal.replace')}</button>
|
|
1948
|
-
<button class="neiki-btn neiki-btn-primary" type="button" data-action="replaceAll">${t('modal.replaceAll')}</button>
|
|
2064
|
+
<button class="neiki-btn neiki-btn-secondary" type="button" data-action="findNext">${Utils.escapeHTML(t('modal.findNext'))}</button>
|
|
2065
|
+
<button class="neiki-btn neiki-btn-secondary" type="button" data-action="replaceOne">${Utils.escapeHTML(t('modal.replace'))}</button>
|
|
2066
|
+
<button class="neiki-btn neiki-btn-primary" type="button" data-action="replaceAll">${Utils.escapeHTML(t('modal.replaceAll'))}</button>
|
|
1949
2067
|
</div>
|
|
1950
2068
|
`;
|
|
1951
2069
|
|
|
@@ -1958,6 +2076,9 @@
|
|
|
1958
2076
|
let currentMatches = [];
|
|
1959
2077
|
let currentIndex = -1;
|
|
1960
2078
|
|
|
2079
|
+
findInput.placeholder = t('modal.searchText');
|
|
2080
|
+
replaceInput.placeholder = t('modal.replacementText');
|
|
2081
|
+
|
|
1961
2082
|
const clearHighlights = () => {
|
|
1962
2083
|
const highlights = this.editor.contentArea.querySelectorAll('.neiki-highlight-find');
|
|
1963
2084
|
highlights.forEach(h => {
|
|
@@ -1978,7 +2099,6 @@
|
|
|
1978
2099
|
return;
|
|
1979
2100
|
}
|
|
1980
2101
|
|
|
1981
|
-
const content = this.editor.contentArea.innerHTML;
|
|
1982
2102
|
let flags = 'g';
|
|
1983
2103
|
if (!caseCheck.checked) flags += 'i';
|
|
1984
2104
|
|
|
@@ -2127,16 +2247,16 @@
|
|
|
2127
2247
|
|
|
2128
2248
|
modal.innerHTML = `
|
|
2129
2249
|
<div class="neiki-modal-header">
|
|
2130
|
-
<h3>${t('menu.help')}</h3>
|
|
2250
|
+
<h3>${Utils.escapeHTML(t('menu.help'))}</h3>
|
|
2131
2251
|
<button class="neiki-modal-close" type="button">${Icons.close}</button>
|
|
2132
2252
|
</div>
|
|
2133
2253
|
<div class="neiki-modal-body" style="text-align: center; padding: 24px 20px;">
|
|
2134
2254
|
<img src="https://github.com/neikiri/neiki-editor/raw/main/logo.png" alt="Neiki's Editor" style="width: 120px; height: auto; margin: 0 auto 16px; display: block;">
|
|
2135
2255
|
<div style="font-size: 14px; line-height: 2; color: var(--neiki-text-primary);">
|
|
2136
|
-
<div><strong>${t('help.author')}:</strong> neikiri (Jindřich Stoklasa)</div>
|
|
2137
|
-
<div><strong>${t('help.version')}:</strong> 2.9.
|
|
2138
|
-
<div><strong>${t('help.github')}:</strong> <a href="https://github.com/neikiri/neiki-editor" target="_blank" style="color: var(--neiki-accent);">github.com/neikiri/neiki-editor</a></div>
|
|
2139
|
-
<div><strong>${t('help.documentation')}:</strong> <a href="https://github.com/neikiri/neiki-editor/wiki" target="_blank" style="color: var(--neiki-accent);">Wiki</a></div>
|
|
2256
|
+
<div><strong>${Utils.escapeHTML(t('help.author'))}:</strong> neikiri (Jindřich Stoklasa)</div>
|
|
2257
|
+
<div><strong>${Utils.escapeHTML(t('help.version'))}:</strong> 2.9.3</div>
|
|
2258
|
+
<div><strong>${Utils.escapeHTML(t('help.github'))}:</strong> <a href="https://github.com/neikiri/neiki-editor" target="_blank" rel="noopener noreferrer" style="color: var(--neiki-accent);">github.com/neikiri/neiki-editor</a></div>
|
|
2259
|
+
<div><strong>${Utils.escapeHTML(t('help.documentation'))}:</strong> <a href="https://github.com/neikiri/neiki-editor/wiki" target="_blank" rel="noopener noreferrer" style="color: var(--neiki-accent);">Wiki</a></div>
|
|
2140
2260
|
</div>
|
|
2141
2261
|
</div>
|
|
2142
2262
|
`;
|
|
@@ -2743,26 +2863,34 @@
|
|
|
2743
2863
|
|
|
2744
2864
|
insertHTML(html) {
|
|
2745
2865
|
this.editor.focus();
|
|
2746
|
-
document.execCommand('insertHTML', false, html);
|
|
2866
|
+
document.execCommand('insertHTML', false, Utils.sanitizeHTML(html));
|
|
2747
2867
|
this.editor.history.record();
|
|
2748
2868
|
this.editor.triggerChange();
|
|
2749
2869
|
}
|
|
2750
2870
|
|
|
2751
2871
|
insertLink(url, text, newTab = false) {
|
|
2872
|
+
if (!Utils.isSafeUrl(url)) return;
|
|
2752
2873
|
const selection = Utils.getSelection();
|
|
2753
2874
|
const range = selection.rangeCount > 0 ? selection.getRangeAt(0) : null;
|
|
2754
2875
|
|
|
2755
2876
|
if (range && !range.collapsed) {
|
|
2756
2877
|
this.exec('createLink', url);
|
|
2757
2878
|
if (newTab) {
|
|
2758
|
-
const
|
|
2759
|
-
links
|
|
2879
|
+
const selectorUrl = window.CSS && CSS.escape ? CSS.escape(url) : url.replace(/"/g, '\\"');
|
|
2880
|
+
const links = this.editor.contentArea.querySelectorAll('a[href="' + selectorUrl + '"]');
|
|
2881
|
+
links.forEach(link => {
|
|
2882
|
+
link.setAttribute('target', '_blank');
|
|
2883
|
+
link.setAttribute('rel', 'noopener noreferrer');
|
|
2884
|
+
});
|
|
2760
2885
|
}
|
|
2761
2886
|
} else {
|
|
2762
2887
|
const link = document.createElement('a');
|
|
2763
2888
|
link.href = url;
|
|
2764
2889
|
link.textContent = text || url;
|
|
2765
|
-
if (newTab)
|
|
2890
|
+
if (newTab) {
|
|
2891
|
+
link.target = '_blank';
|
|
2892
|
+
link.rel = 'noopener noreferrer';
|
|
2893
|
+
}
|
|
2766
2894
|
|
|
2767
2895
|
this.editor.focus();
|
|
2768
2896
|
document.execCommand('insertHTML', false, link.outerHTML);
|
|
@@ -2772,9 +2900,10 @@
|
|
|
2772
2900
|
}
|
|
2773
2901
|
|
|
2774
2902
|
insertImage(url, alt = '', width = '') {
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
if (
|
|
2903
|
+
if (!Utils.isSafeUrl(url, true)) return;
|
|
2904
|
+
let html = `<img src="${Utils.escapeHTML(url)}"`;
|
|
2905
|
+
if (alt) html += ` alt="${Utils.escapeHTML(alt)}"`;
|
|
2906
|
+
if (width) html += ` width="${Utils.escapeHTML(width)}"`;
|
|
2778
2907
|
html += '>';
|
|
2779
2908
|
|
|
2780
2909
|
this.editor.focus();
|
|
@@ -2841,6 +2970,8 @@
|
|
|
2841
2970
|
'neiki_' + (typeof element === 'string' ? element.replace(/[^a-zA-Z0-9]/g, '_') : 'editor');
|
|
2842
2971
|
|
|
2843
2972
|
this.config = Utils.deepMerge(DEFAULT_CONFIG, options);
|
|
2973
|
+
this.instanceIndex = ++EDITOR_INSTANCE_COUNTER;
|
|
2974
|
+
this.storageId = this.createAutosaveStorageId(element);
|
|
2844
2975
|
this.isFullscreen = false;
|
|
2845
2976
|
this.isAutosaveEnabled = false;
|
|
2846
2977
|
this.autosaveInterval = null;
|
|
@@ -2851,7 +2982,7 @@
|
|
|
2851
2982
|
|
|
2852
2983
|
init() {
|
|
2853
2984
|
// Initialize storage first
|
|
2854
|
-
this.storage = new StorageManager(this.
|
|
2985
|
+
this.storage = new StorageManager(this.storageId);
|
|
2855
2986
|
|
|
2856
2987
|
// Set language for translations
|
|
2857
2988
|
_currentLanguage = this.config.language || 'en';
|
|
@@ -2924,6 +3055,78 @@
|
|
|
2924
3055
|
this.originalElement.parentNode.insertBefore(this.container, this.originalElement);
|
|
2925
3056
|
}
|
|
2926
3057
|
|
|
3058
|
+
createAutosaveStorageId(element) {
|
|
3059
|
+
const customKey = this.config.autosaveKey ||
|
|
3060
|
+
this.originalElement.getAttribute('data-neiki-autosave-key');
|
|
3061
|
+
|
|
3062
|
+
if (customKey) {
|
|
3063
|
+
return 'autosave_' + this.normalizeStorageKey(customKey);
|
|
3064
|
+
}
|
|
3065
|
+
|
|
3066
|
+
const pageScope = this.hashString(this.getPageStorageScope());
|
|
3067
|
+
const elementScope = this.normalizeStorageKey(this.getElementStorageScope(element));
|
|
3068
|
+
return 'autosave_' + pageScope + '_' + elementScope;
|
|
3069
|
+
}
|
|
3070
|
+
|
|
3071
|
+
getPageStorageScope() {
|
|
3072
|
+
try {
|
|
3073
|
+
return window.location.href || window.location.pathname || 'page';
|
|
3074
|
+
} catch (e) {
|
|
3075
|
+
return 'page';
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
|
|
3079
|
+
getElementStorageScope(element) {
|
|
3080
|
+
if (this.originalElement.id) return this.originalElement.id;
|
|
3081
|
+
if (this.originalElement.name) return this.originalElement.name;
|
|
3082
|
+
if (this.originalElement.getAttribute('data-neiki-id')) {
|
|
3083
|
+
return this.originalElement.getAttribute('data-neiki-id');
|
|
3084
|
+
}
|
|
3085
|
+
if (typeof element === 'string') return element;
|
|
3086
|
+
return this.getElementPath(this.originalElement);
|
|
3087
|
+
}
|
|
3088
|
+
|
|
3089
|
+
getElementPath(element) {
|
|
3090
|
+
const parts = [];
|
|
3091
|
+
let node = element;
|
|
3092
|
+
|
|
3093
|
+
while (node && node.nodeType === Node.ELEMENT_NODE && node !== document.body) {
|
|
3094
|
+
let part = node.tagName.toLowerCase();
|
|
3095
|
+
const parent = node.parentNode;
|
|
3096
|
+
|
|
3097
|
+
if (parent && parent.children) {
|
|
3098
|
+
const siblings = Array.prototype.filter.call(parent.children, child => child.tagName === node.tagName);
|
|
3099
|
+
if (siblings.length > 1) {
|
|
3100
|
+
part += ':nth-of-type(' + (siblings.indexOf(node) + 1) + ')';
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
|
|
3104
|
+
parts.unshift(part);
|
|
3105
|
+
node = parent;
|
|
3106
|
+
}
|
|
3107
|
+
|
|
3108
|
+
return parts.length ? parts.join('>') : 'editor_' + this.instanceIndex;
|
|
3109
|
+
}
|
|
3110
|
+
|
|
3111
|
+
normalizeStorageKey(value) {
|
|
3112
|
+
return String(value)
|
|
3113
|
+
.trim()
|
|
3114
|
+
.replace(/[^a-zA-Z0-9_-]+/g, '_')
|
|
3115
|
+
.replace(/^_+|_+$/g, '') || 'editor';
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3118
|
+
hashString(value) {
|
|
3119
|
+
let hash = 0;
|
|
3120
|
+
const input = String(value);
|
|
3121
|
+
|
|
3122
|
+
for (let i = 0; i < input.length; i++) {
|
|
3123
|
+
hash = ((hash << 5) - hash) + input.charCodeAt(i);
|
|
3124
|
+
hash |= 0;
|
|
3125
|
+
}
|
|
3126
|
+
|
|
3127
|
+
return Math.abs(hash).toString(36);
|
|
3128
|
+
}
|
|
3129
|
+
|
|
2927
3130
|
createToolbar() {
|
|
2928
3131
|
this.toolbar = Utils.createElement('div', { className: 'neiki-toolbar' });
|
|
2929
3132
|
this.toolbarButtons = {};
|
|
@@ -3357,14 +3560,14 @@
|
|
|
3357
3560
|
|
|
3358
3561
|
if (autosaveEnabled && autosavedContent) {
|
|
3359
3562
|
// Restore autosaved content only if autosave was enabled
|
|
3360
|
-
this.contentArea.innerHTML = autosavedContent;
|
|
3563
|
+
this.contentArea.innerHTML = Utils.sanitizeHTML(autosavedContent);
|
|
3361
3564
|
} else {
|
|
3362
3565
|
// Always use original element content (textarea value or innerHTML)
|
|
3363
3566
|
// This ensures the page's actual content is shown, not old localStorage data
|
|
3364
3567
|
if (this.originalElement.value) {
|
|
3365
|
-
this.contentArea.innerHTML = this.originalElement.value;
|
|
3568
|
+
this.contentArea.innerHTML = Utils.sanitizeHTML(this.originalElement.value);
|
|
3366
3569
|
} else if (this.originalElement.innerHTML.trim()) {
|
|
3367
|
-
this.contentArea.innerHTML = this.originalElement.innerHTML;
|
|
3570
|
+
this.contentArea.innerHTML = Utils.sanitizeHTML(this.originalElement.innerHTML);
|
|
3368
3571
|
}
|
|
3369
3572
|
}
|
|
3370
3573
|
|
|
@@ -5026,7 +5229,6 @@
|
|
|
5026
5229
|
if (!this.isResizing || !this.currentImg) return;
|
|
5027
5230
|
|
|
5028
5231
|
const dx = e.clientX - this.startX;
|
|
5029
|
-
const dy = e.clientY - this.startY;
|
|
5030
5232
|
|
|
5031
5233
|
let newWidth, newHeight;
|
|
5032
5234
|
|