diff --git a/dist/w2ui.js b/dist/w2ui.js
index 12629fe8f..63abc5fa3 100644
--- a/dist/w2ui.js
+++ b/dist/w2ui.js
@@ -1,4 +1,4 @@
-/* w2ui 2.0.x (nightly) (10/31/2025, 8:43:03 AM) (c) http://w2ui.com, vitmalina@gmail.com */
+/* w2ui 2.0.x (nightly) (11/23/2025, 8:17:07 PM) (c) http://w2ui.com, vitmalina@gmail.com */
/**
* Part of w2ui 2.0 library
* - Dependencies: w2utils
@@ -393,6 +393,8 @@ const w2locale = {
'record': '---',
'records': '---',
'Refreshing...': '---',
+ 'RegEx': '---',
+ 'regex': '---',
'Reload data in the list': '---',
'Remove': '---',
'Remove This Field': '---',
@@ -2569,10 +2571,11 @@ class Utils {
}
return str.replace(/\${([^}]+)?}/g, function($1, $2) { return replace_obj[$2]||$2 })
}
- marker(el, items, options = { onlyFirst: false, wholeWord: false }) {
+ marker(el, items, options = { onlyFirst: false, wholeWord: false, isRegex: false}) {
options.tag ??= 'span'
options.class ??= 'w2ui-marker'
options.raplace = (matched) => `<${options.tag} class="${options.class}">${matched}${options.tag}>`
+ const isRegexSearch = options.isRegex || false;
if (!Array.isArray(items)) {
if (items != null && items !== '') {
items = [items]
@@ -2583,14 +2586,114 @@ class Utils {
if (typeof el == 'string') {
_clearMerkers(el)
items.forEach(item => {
- el = _replace(el, item, options.raplace)
+ if (isRegexSearch) {
+ // For regex searches with string elements
+ try {
+ let flags = 'i' + (!options.onlyFirst ? 'g' : '')
+ let regex = new RegExp(item, flags)
+ el = el.replace(regex, options.raplace)
+ } catch (e) {
+ console.error('Invalid regular expression:', e)
+ // Fallback to standard replace
+ el = _replace(el, item, options.raplace)
+ }
+ } else {
+ // Standard string replace
+ el = _replace(el, item, options.raplace)
+ }
})
} else {
query(el).each(el => {
_clearMerkers(el)
- items.forEach(item => {
- el.innerHTML = _replace(el.innerHTML, item, options.raplace)
- })
+ if (isRegexSearch) {
+ // For regex searches, use DOM traversal approach
+ items.forEach(pattern => {
+ try {
+ let flags = 'i' // Always case-insensitive
+ if (!options.onlyFirst) {
+ flags += 'g' // Add 'g' for global unless onlyFirst is true
+ }
+ if (options.wholeWord) {
+ // If wholeWord is true, wrap the pattern with word boundary markers
+ pattern = '\b' + pattern + '\b'
+ }
+ let regex = new RegExp(pattern, flags)
+ // Get all text nodes
+ let textNodes = []
+ function getTextNodes(node) {
+ if (node.nodeType === 3) { // Text node
+ textNodes.push(node)
+ } else if (node.nodeType === 1) { // Element node
+ // Skip script and style tags
+ if (node.tagName !== 'SCRIPT' && node.tagName !== 'STYLE') {
+ for (let i = 0; i < node.childNodes.length; i++) {
+ getTextNodes(node.childNodes[i])
+ }
+ }
+ }
+ }
+ getTextNodes(el)
+ // Process each text node
+ textNodes.forEach(textNode => {
+ let text = textNode.nodeValue
+ let matches = []
+ let match
+ // Find all matches
+ if (options.onlyFirst) {
+ match = regex.exec(text)
+ if (match) matches.push({
+ index: match.index,
+ text: match[0]
+ })
+ } else {
+ while ((match = regex.exec(text)) !== null) {
+ matches.push({
+ index: match.index,
+ text: match[0]
+ })
+ }
+ }
+ // Apply highlighting
+ if (matches.length > 0) {
+ let parent = textNode.parentNode
+ let fragment = document.createDocumentFragment()
+ let lastIndex = 0
+ matches.forEach(match => {
+ // Add text before match
+ if (match.index > lastIndex) {
+ fragment.appendChild(document.createTextNode(
+ text.substring(lastIndex, match.index)
+ ))
+ }
+ // Add highlighted match
+ let span = document.createElement(options.tag)
+ span.className = options.class
+ span.appendChild(document.createTextNode(match.text))
+ fragment.appendChild(span)
+ lastIndex = match.index + match.text.length
+ })
+ // Add remaining text
+ if (lastIndex < text.length) {
+ fragment.appendChild(document.createTextNode(
+ text.substring(lastIndex)
+ ))
+ }
+ // Replace the text node with our fragment
+ parent.replaceChild(fragment, textNode)
+ }
+ })
+ } catch (e) {
+ console.error('Invalid regular expression:', e)
+ // Fallback to standard innerHTML replace
+ el.innerHTML = _replace(el.innerHTML, pattern, options.raplace)
+ }
+ })
+ } else {
+ // Standard innerHTML replace for non-regex
+ items.forEach(item => {
+ el.innerHTML = _replace(el.innerHTML, item, options.raplace)
+ })
+ }
})
}
return el
@@ -5579,7 +5682,7 @@ class MenuTooltip extends Tooltip {
menuStyle : '',
search : false, // search input inside tooltip
filter : false, // will apply filter, if anchor is INPUT or TEXTAREA
- match : 'contains', // is, begins, ends, contains
+ match : 'contains', // is, begins, ends, contains, regexp
markSearch : false,
prefilter : false,
altRows : false,
@@ -6252,18 +6355,29 @@ class MenuTooltip extends Tooltip {
return prom
}
items.forEach(item => {
- let prefix = ''
- let suffix = ''
- if (['is', 'begins', 'begins with'].indexOf(options.match) !== -1) prefix = '^'
- if (['is', 'ends', 'ends with'].indexOf(options.match) !== -1) suffix = '$'
- try {
- let re = new RegExp(prefix + search + suffix, 'i')
- if (re.test(item.text) || item.text === '...') {
- item.hidden = false
- } else {
- item.hidden = true
- }
- } catch (e) {}
+ if (options.match == 'regex') {
+ try {
+ let re = new RegExp(search, 'i')
+ if (re.test(item.text) || item.text === '...') {
+ item.hidden = false
+ } else {
+ item.hidden = true
+ }
+ } catch (e) {}
+ } else {
+ let prefix = ''
+ let suffix = ''
+ if (['is', 'begins', 'begins with'].indexOf(options.match) !== -1) prefix = '^'
+ if (['is', 'ends', 'ends with'].indexOf(options.match) !== -1) suffix = '$'
+ try {
+ let re = new RegExp(prefix + search + suffix, 'i')
+ if (re.test(item.text) || item.text === '...') {
+ item.hidden = false
+ } else {
+ item.hidden = true
+ }
+ } catch (e) {}
+ }
// do not show selected items
if (options.hideSelected && selectedIds.includes(item.id)) {
item.hidden = true
diff --git a/gulpfile.js b/gulpfile.js
index 59b7bdbb5..7328390b7 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -1,7 +1,8 @@
/* eslint-env node */
const gulp = require('gulp')
const header = require('gulp-header')
-const iconfont = require('gulp-iconfont')
+// lazy load iconfont to avoid ESM compatibility issues
+let iconfont = null
const less = require('gulp-less')
const cleanCSS = require('gulp-clean-css')
const uglify = require('gulp-uglify')
@@ -175,6 +176,10 @@ let tasks = {
},
icons(cb) {
+ // Lazy load iconfont
+ if (!iconfont) {
+ iconfont = require('gulp-iconfont')
+ }
let fs = require('fs')
let css = `@font-face {
font-family: "w2ui-font";
diff --git a/package.json b/package.json
index d745114f1..e207c2593 100644
--- a/package.json
+++ b/package.json
@@ -21,13 +21,13 @@
"del": "^6.0.0",
"eslint": "^8.25.0",
"eslint-plugin-align-assignments": "^1.1.2",
- "gulp": "^4.0.2",
+ "gulp": "^5.0.1",
"gulp-babel": "^8.0.0",
"gulp-clean-css": "^4.3.0",
"gulp-concat": "^2.6.1",
- "gulp-header": "^2.0.9",
- "gulp-iconfont": "^11.0.0",
- "gulp-less": "^4.0.1",
+ "gulp-header": "^1.8.9",
+ "gulp-iconfont": "^12.0.0",
+ "gulp-less": "^5.0.0",
"gulp-rename": "^2.0.0",
"gulp-replace": "^1.0.0",
"gulp-uglify": "^3.0.2",
diff --git a/patch.diff b/patch.diff
new file mode 100644
index 000000000..f5ccc0d3e
Binary files /dev/null and b/patch.diff differ
diff --git a/src/w2locale.js b/src/w2locale.js
index 0343c5431..d3c75b478 100644
--- a/src/w2locale.js
+++ b/src/w2locale.js
@@ -95,6 +95,8 @@ const w2locale = {
'record': '---',
'records': '---',
'Refreshing...': '---',
+ 'RegEx': '---',
+ 'regex': '---',
'Reload data in the list': '---',
'Remove': '---',
'Remove This Field': '---',
diff --git a/src/w2tooltip.js b/src/w2tooltip.js
index 70a70f1c3..9c308bab8 100644
--- a/src/w2tooltip.js
+++ b/src/w2tooltip.js
@@ -1441,7 +1441,7 @@ class MenuTooltip extends Tooltip {
menuStyle : '',
search : false, // search input inside tooltip
filter : false, // will apply filter, if anchor is INPUT or TEXTAREA
- match : 'contains', // is, begins, ends, contains
+ match : 'contains', // is, begins, ends, contains, regexp
markSearch : false,
prefilter : false,
altRows : false,
@@ -2126,18 +2126,29 @@ class MenuTooltip extends Tooltip {
return prom
}
items.forEach(item => {
- let prefix = ''
- let suffix = ''
- if (['is', 'begins', 'begins with'].indexOf(options.match) !== -1) prefix = '^'
- if (['is', 'ends', 'ends with'].indexOf(options.match) !== -1) suffix = '$'
- try {
- let re = new RegExp(prefix + search + suffix, 'i')
- if (re.test(item.text) || item.text === '...') {
- item.hidden = false
- } else {
- item.hidden = true
- }
- } catch (e) {}
+ if (options.match == 'regex') {
+ try {
+ let re = new RegExp(search, 'i')
+ if (re.test(item.text) || item.text === '...') {
+ item.hidden = false
+ } else {
+ item.hidden = true
+ }
+ } catch (e) {}
+ } else {
+ let prefix = ''
+ let suffix = ''
+ if (['is', 'begins', 'begins with'].indexOf(options.match) !== -1) prefix = '^'
+ if (['is', 'ends', 'ends with'].indexOf(options.match) !== -1) suffix = '$'
+ try {
+ let re = new RegExp(prefix + search + suffix, 'i')
+ if (re.test(item.text) || item.text === '...') {
+ item.hidden = false
+ } else {
+ item.hidden = true
+ }
+ } catch (e) {}
+ }
// do not show selected items
if (options.hideSelected && selectedIds.includes(item.id)) {
item.hidden = true
diff --git a/src/w2utils.js b/src/w2utils.js
index 171b910fb..36c386887 100644
--- a/src/w2utils.js
+++ b/src/w2utils.js
@@ -1656,10 +1656,12 @@ class Utils {
return str.replace(/\${([^}]+)?}/g, function($1, $2) { return replace_obj[$2]||$2 })
}
- marker(el, items, options = { onlyFirst: false, wholeWord: false }) {
+ marker(el, items, options = { onlyFirst: false, wholeWord: false, isRegex: false}) {
options.tag ??= 'span'
options.class ??= 'w2ui-marker'
options.raplace = (matched) => `<${options.tag} class="${options.class}">${matched}${options.tag}>`
+
+ const isRegexSearch = options.isRegex || false;
if (!Array.isArray(items)) {
if (items != null && items !== '') {
items = [items]
@@ -1670,14 +1672,125 @@ class Utils {
if (typeof el == 'string') {
_clearMerkers(el)
items.forEach(item => {
- el = _replace(el, item, options.raplace)
+ if (isRegexSearch) {
+ // For regex searches with string elements
+ try {
+ let flags = 'i' + (!options.onlyFirst ? 'g' : '')
+ let regex = new RegExp(item, flags)
+ el = el.replace(regex, options.raplace)
+ } catch (e) {
+ console.error('Invalid regular expression:', e)
+ // Fallback to standard replace
+ el = _replace(el, item, options.raplace)
+ }
+ } else {
+ // Standard string replace
+ el = _replace(el, item, options.raplace)
+ }
})
} else {
query(el).each(el => {
_clearMerkers(el)
- items.forEach(item => {
- el.innerHTML = _replace(el.innerHTML, item, options.raplace)
- })
+ if (isRegexSearch) {
+ // For regex searches, use DOM traversal approach
+ items.forEach(pattern => {
+ try {
+ let flags = 'i' // Always case-insensitive
+ if (!options.onlyFirst) {
+ flags += 'g' // Add 'g' for global unless onlyFirst is true
+ }
+ if (options.wholeWord) {
+ // If wholeWord is true, wrap the pattern with word boundary markers
+ pattern = '\b' + pattern + '\b'
+ }
+
+ let regex = new RegExp(pattern, flags)
+
+ // Get all text nodes
+ let textNodes = []
+ function getTextNodes(node) {
+ if (node.nodeType === 3) { // Text node
+ textNodes.push(node)
+ } else if (node.nodeType === 1) { // Element node
+ // Skip script and style tags
+ if (node.tagName !== 'SCRIPT' && node.tagName !== 'STYLE') {
+ for (let i = 0; i < node.childNodes.length; i++) {
+ getTextNodes(node.childNodes[i])
+ }
+ }
+ }
+ }
+
+ getTextNodes(el)
+
+ // Process each text node
+ textNodes.forEach(textNode => {
+ let text = textNode.nodeValue
+ let matches = []
+ let match
+
+ // Find all matches
+ if (options.onlyFirst) {
+ match = regex.exec(text)
+ if (match) matches.push({
+ index: match.index,
+ text: match[0]
+ })
+ } else {
+ while ((match = regex.exec(text)) !== null) {
+ matches.push({
+ index: match.index,
+ text: match[0]
+ })
+ }
+ }
+
+ // Apply highlighting
+ if (matches.length > 0) {
+ let parent = textNode.parentNode
+ let fragment = document.createDocumentFragment()
+ let lastIndex = 0
+
+ matches.forEach(match => {
+ // Add text before match
+ if (match.index > lastIndex) {
+ fragment.appendChild(document.createTextNode(
+ text.substring(lastIndex, match.index)
+ ))
+ }
+
+ // Add highlighted match
+ let span = document.createElement(options.tag)
+ span.className = options.class
+ span.appendChild(document.createTextNode(match.text))
+ fragment.appendChild(span)
+
+ lastIndex = match.index + match.text.length
+ })
+
+ // Add remaining text
+ if (lastIndex < text.length) {
+ fragment.appendChild(document.createTextNode(
+ text.substring(lastIndex)
+ ))
+ }
+
+ // Replace the text node with our fragment
+ parent.replaceChild(fragment, textNode)
+ }
+ })
+ } catch (e) {
+ console.error('Invalid regular expression:', e)
+ // Fallback to standard innerHTML replace
+ el.innerHTML = _replace(el.innerHTML, pattern, options.raplace)
+ }
+ })
+ } else {
+ // Standard innerHTML replace for non-regex
+ items.forEach(item => {
+ el.innerHTML = _replace(el.innerHTML, item, options.raplace)
+ })
+ }
})
}
return el