(()=>{let l=window.srfmPreviewStyling?.containerId;if(l&&/^[a-zA-Z0-9_-]+$/.test(l)){let f=document.querySelector("."+l);if(f){let a=f.className,d=document.createElement("style"),t=(d.id="srfm-preview-override-style",f.appendChild(d),0);function r(){var r=f.offsetHeight;if(r&&!(Math.abs(r-t)<4)){let o={type:"srfm-preview-height",height:t=r};[window.parent,window.top].forEach(function(r){r&&r!==window&&r.postMessage(o,"*")})}}window.parent!==window&&(r(),window.addEventListener("load",r),window.addEventListener("resize",r)),window.addEventListener("message",function(r){if(r.origin===window.location.origin&&r.data)if("srfm-reset-styling"===r.data.type)d.textContent="",f.className=a,(o=f.querySelector(".srfm-submit-container .wp-block-button"))&&(o.removeAttribute("style"),o=o.querySelector("button"))&&o.removeAttribute("style");else if("srfm-update-styling"===r.data.type){var o=r.data.styling;if(o){var t={},r=(o.primaryColor&&(r="hsl(from "+o.primaryColor+" h s l / ",t["--srfm-color-scheme-primary"]=o.primaryColor,t["--srfm-quill-editor-color"]=o.primaryColor,t["--srfm-color-input-border-hover"]=r+"0.65)",t["--srfm-color-input-border-focus-glow"]=r+"0.15)",t["--srfm-color-input-selected"]=r+"0.1)",t["--srfm-btn-color-hover"]=r+"0.9)",t["--srfm-btn-color-disabled"]=r+"0.25)"),o.textColor&&(r="hsl(from "+o.textColor+" h s l / ",t["--srfm-color-scheme-text"]=o.textColor,t["--srfm-color-input-label"]=o.textColor,t["--srfm-color-input-text"]=o.textColor,t["--srfm-color-input-description"]=r+"0.65)",t["--srfm-color-input-placeholder"]=r+"0.5)",t["--srfm-color-input-prefix"]=r+"0.65)",t["--srfm-color-input-background"]=r+"0.02)",t["--srfm-color-input-background-hover"]=r+"0.05)",t["--srfm-color-input-background-disabled"]=r+"0.07)",t["--srfm-color-input-border"]=r+"0.25)",t["--srfm-color-input-border-disabled"]=r+"0.15)",t["--srfm-color-multi-choice-svg"]=r+"0.7)"),o.textOnPrimaryColor&&(t["--srfm-color-scheme-text-on-primary"]=o.textOnPrimaryColor),o.formPaddingUnit||"px"),e=o.formBorderRadiusUnit||"px";if(void 0!==o.formPaddingTop&&(t["--srfm-form-padding-top"]=o.formPaddingTop+r),void 0!==o.formPaddingRight&&(t["--srfm-form-padding-right"]=o.formPaddingRight+r),void 0!==o.formPaddingBottom&&(t["--srfm-form-padding-bottom"]=o.formPaddingBottom+r),void 0!==o.formPaddingLeft&&(t["--srfm-form-padding-left"]=o.formPaddingLeft+r),void 0!==o.formBorderRadiusTop&&(t["--srfm-form-border-radius-top"]=o.formBorderRadiusTop+e),void 0!==o.formBorderRadiusRight&&(t["--srfm-form-border-radius-right"]=o.formBorderRadiusRight+e),void 0!==o.formBorderRadiusBottom&&(t["--srfm-form-border-radius-bottom"]=o.formBorderRadiusBottom+e),void 0!==o.formBorderRadiusLeft&&(t["--srfm-form-border-radius-left"]=o.formBorderRadiusLeft+e),f.classList.remove("srfm-bg-color","srfm-bg-gradient","srfm-bg-image"),"color"===o.bgType?(f.classList.add("srfm-bg-color"),o.bgColor&&(t["--srfm-bg-color"]=o.bgColor)):"gradient"===o.bgType?(f.classList.add("srfm-bg-gradient"),o.bgGradient&&(t["--srfm-bg-gradient"]=o.bgGradient)):"image"===o.bgType&&(f.classList.add("srfm-bg-image"),o.bgImage&&((r=o.bgImage.replace(/"/g,"")).startsWith("https://")||r.startsWith("http://"))&&(t["--srfm-bg-image"]='url("'+r+'")'),o.bgImagePosition&&(e=100*(o.bgImagePosition.x??.5),r=100*(o.bgImagePosition.y??.5),t["--srfm-bg-position"]=e+"% "+r+"%"),o.bgImageSize&&(t["--srfm-bg-size"]=o.bgImageSize),o.bgImageRepeat&&(t["--srfm-bg-repeat"]=o.bgImageRepeat),o.bgImageAttachment)&&(t["--srfm-bg-attachment"]=o.bgImageAttachment),o.fieldSpacing){var e=window.srfmPreviewStyling?.fieldSpacingVars;if(e){var i,r=e.small||{},e=e[o.fieldSpacing]||{},s=Object.assign({},r,e);for(i in s)i.startsWith("--")&&(t[i]=s[i])}}{var n=t;let r="."+l+" {\n";for(var m in n)null!=n[m]&&(r+="\t"+m+": "+n[m]+";\n");r+="}\n",d.textContent=r}o.buttonAlignment&&(r=f.querySelector(".srfm-submit-container .wp-block-button"))&&(r.style.textAlign="justify"===o.buttonAlignment?"center":o.buttonAlignment,e=r.querySelector("button"))&&(e.style.width="justify"===o.buttonAlignment?"100%":"");r=new CustomEvent("srfm-preview-styling-update",{detail:{styling:o,container:f}});document.dispatchEvent(r)}}})}}})();/** * List block tracker for UUID-aware list editing and serialization. * * Dependencies: wp.blocks, SFE.ElementPrep * Exposes: SFE.ListBlockTracker */ (function() { 'use strict'; window.MWP = window.MWP || {}; window.MWP.SFE = window.MWP.SFE || {}; const SFE = window.MWP.SFE; SFE.ManagerData = SFE.ManagerData || {}; const { createBlock, serialize: serializeBlocks } = window.wp?.blocks || {}; const UUID_ATTR_KEYS = ['mwpSfeUuid', 'mwpSfeUuidShadow']; const LIST_ITEM_TEXT_ATTR = 'data-mwp-sfe-list-item-text'; const LIST_ITEM_TEXT_CLASS = 'mwp-sfe-list-item-text'; const PLACEHOLDER_ATTR = 'data-rich-text-placeholder'; const LIST_ID_ATTR = 'data-list-id'; const LIST_ITEM_ID_ATTR = 'data-item-id'; const LIST_RUNTIME_UUID_ATTR = 'data-mwp-sfe-list-runtime-uuid'; const LIST_ITEM_RUNTIME_UUID_ATTR = 'data-mwp-sfe-list-item-runtime-uuid'; /** * Return whether one node is a nested list element. * * @param {Node|null} node Candidate DOM node. * @returns {boolean} True when the node is a nested list. */ function isNestedListNode(node) { return !!( node && node.nodeType === Node.ELEMENT_NODE && (node.tagName === 'UL' || node.tagName === 'OL') ); } /** * Return whether one node is ignorable formatting whitespace between list * structures. * * Pretty-printed block markup often leaves direct `\n` text nodes between a * list item's own text and its nested child list. Those nodes should not be * moved into the direct text surface because they become visible editing * artifacts once wrapped. * * @param {Node|null} node Candidate DOM node. * @returns {boolean} True when the node is ignorable whitespace. */ function isIgnorableListWhitespaceNode(node) { return !!( node && node.nodeType === Node.TEXT_NODE && String(node.textContent || '') .replace(/\uFEFF/g, '') .replace(/\u00A0/g, ' ') .trim() .length === 0 ); } /** * Return the direct inline text surface for one list item. * * @param {HTMLLIElement|null} liElement Candidate list item. * @returns {HTMLElement|null} Existing direct text surface, if any. */ function getDirectItemTextSurface(liElement) { if (!liElement || liElement.nodeType !== Node.ELEMENT_NODE || liElement.tagName !== 'LI') { return null; } return Array.from(liElement.children || []).find(child => ( child && child.nodeType === Node.ELEMENT_NODE && child.getAttribute(LIST_ITEM_TEXT_ATTR) === '1' )) || null; } /** * Return the DOM attribute name that stores one session-scoped runtime UUID. * * Runtime UUIDs are distinct from the existing serialization/attr UUIDs. They * exist only while the editor session is open so external callers can point at * the current live list cursor/target without having to manage shifting paths. * * @param {string} type Supported runtime identity type. * @returns {string} Attribute name, or an empty string for invalid types. */ function getRuntimeUuidAttributeName(type) { if (type === 'list') { return LIST_RUNTIME_UUID_ATTR; } if (type === 'item') { return LIST_ITEM_RUNTIME_UUID_ATTR; } return ''; } /** * Normalize one runtime UUID candidate. * * @param {*} value Candidate runtime UUID. * @returns {string} Trimmed runtime UUID, or an empty string. */ function normalizeRuntimeUuid(value) { return String(value || '').trim(); } /** * Return whether one element matches the requested runtime-identity type. * * @param {Element|null} element Candidate DOM element. * @param {string} type Supported runtime identity type. * @returns {boolean} True when the element matches the type. */ function isRuntimeIdentityElement(element, type) { if (!element || element.nodeType !== Node.ELEMENT_NODE) { return false; } if (type === 'list') { return element.tagName === 'UL' || element.tagName === 'OL'; } if (type === 'item') { return element.tagName === 'LI'; } return false; } /** * Return one tracked element by runtime UUID from the live DOM. * * This intentionally queries the current DOM first instead of trusting only a * cached map so history restores and other structural replacements can keep * runtime UUID resolution stable as long as the attributes remain present. * * @param {Object|null} tracker Active list tracker. * @param {string} runtimeUuid Session-scoped runtime UUID. * @param {string} type Supported runtime identity type. * @returns {Element|null} Matching live DOM element. */ function findElementByRuntimeUuid(tracker, runtimeUuid, type) { const normalizedRuntimeUuid = normalizeRuntimeUuid(runtimeUuid); const attrName = getRuntimeUuidAttributeName(type); if ( !tracker?.listElement || !normalizedRuntimeUuid || !attrName || !isRuntimeIdentityElement(tracker.listElement, 'list') ) { return null; } if ( type === 'list' && tracker.listElement.getAttribute(attrName) === normalizedRuntimeUuid ) { return tracker.listElement; } try { const selector = `[${attrName}="${CSS.escape(normalizedRuntimeUuid)}"]`; return tracker.listElement.querySelector(selector); } catch (error) { const escapedUuid = normalizedRuntimeUuid.replace(/"/g, '\\"'); return tracker.listElement.querySelector(`[${attrName}="${escapedUuid}"]`); } } /** * Return every direct text surface currently attached to one list item. * * Merge/delete flows can temporarily move multiple direct text surfaces into * the same list item. The shared list normalizer must collapse them back to * one canonical surface before placeholder syncing or serialization. * * @param {HTMLLIElement|null} liElement Candidate list item. * @returns {HTMLElement[]} Direct text surfaces in DOM order. */ function getAllDirectItemTextSurfaces(liElement) { if (!liElement || liElement.nodeType !== Node.ELEMENT_NODE || liElement.tagName !== 'LI') { return []; } return Array.from(liElement.children || []).filter(child => ( child && child.nodeType === Node.ELEMENT_NODE && child.getAttribute(LIST_ITEM_TEXT_ATTR) === '1' )); } /** * Return whether one node is only placeholder/caret scaffolding. * * Duplicate direct text surfaces may carry empty placeholder anchors or * caret `
` nodes. Those artifacts should be dropped when collapsing * duplicate surfaces instead of being concatenated into visible stacks. * * @param {Node|null} node Candidate DOM node. * @returns {boolean} True when the node is redundant placeholder UI. */ function isRedundantSurfaceArtifact(node) { if (!node) { return true; } if (isIgnorableListWhitespaceNode(node)) { return true; } if (node.nodeType === Node.TEXT_NODE) { return String(node.textContent || '') .replace(/\uFEFF/g, '') .replace(/\u00A0/g, ' ') .trim() .length === 0; } if (node.nodeType !== Node.ELEMENT_NODE) { return false; } if (node.tagName === 'BR') { return true; } return !!node.hasAttribute?.(PLACEHOLDER_ATTR); } /** * Ensure one list item owns one direct inline text surface. * * The root list remains the single live editor host, but this wrapper gives * schema/ABE flows one stable DOM surface per list item's own text content. * * @param {HTMLLIElement|null} liElement Candidate list item. * @returns {HTMLElement|null} Ensured direct text surface. */ function ensureDirectItemTextSurface(liElement) { if (!liElement || liElement.nodeType !== Node.ELEMENT_NODE || liElement.tagName !== 'LI') { return null; } let surface = getDirectItemTextSurface(liElement); if (!surface) { surface = document.createElement('span'); surface.setAttribute(LIST_ITEM_TEXT_ATTR, '1'); surface.classList.add(LIST_ITEM_TEXT_CLASS); liElement.insertBefore( surface, Array.from(liElement.childNodes || []).find(isNestedListNode) || null ); } getAllDirectItemTextSurfaces(liElement) .filter(candidate => candidate && candidate !== surface) .forEach(duplicateSurface => { Array.from(duplicateSurface.childNodes || []).forEach(node => { if (isRedundantSurfaceArtifact(node)) { node.remove(); return; } surface.appendChild(node); }); duplicateSurface.remove(); }); const childNodes = Array.from(liElement.childNodes || []); childNodes.forEach(node => { if ( node && node !== surface && !isNestedListNode(node) && isIgnorableListWhitespaceNode(node) ) { node.remove(); } }); const movableNodes = childNodes.filter(node => { if (!node || node === surface || isNestedListNode(node)) { return false; } if (isIgnorableListWhitespaceNode(node)) { return false; } return !( node.nodeType === Node.ELEMENT_NODE && node.getAttribute?.(LIST_ITEM_TEXT_ATTR) === '1' ); }); movableNodes.forEach(node => surface.appendChild(node)); return surface; } /** * Clone list attrs while optionally stripping plugin UUID ownership. * * The outermost core/list is the only persisted UUID owner for an entire * list tree. Nested core/list blocks are structural children of that root. * If we preserve a nested list UUID here, one accidental assignment can be * re-serialized forever and split a single logical list into multiple * history/edit targets. Keep this guard unless the ownership model changes * everywhere else in PHP and JS at the same time. * * @param {Object} attrs Parsed Gutenberg attrs. * @param {boolean} allowUuidOwnership True for the root list only. * @returns {Object} Safe cloned attrs. */ function cloneListAttrs(attrs, allowUuidOwnership = true) { const clonedAttrs = JSON.parse(JSON.stringify(attrs || {})); if (allowUuidOwnership) { return clonedAttrs; } UUID_ATTR_KEYS.forEach(key => delete clonedAttrs[key]); return clonedAttrs; } /** * Return the direct list-item children for one list element. * * @param {HTMLElement|null} listElement Candidate list element. * @returns {HTMLLIElement[]} Direct child list items. */ function getDirectListItems(listElement) { if (!listElement || listElement.nodeType !== Node.ELEMENT_NODE) { return []; } return Array.from(listElement.children || []).filter(child => child.tagName === 'LI'); } /** * Normalize one path-like value into zero-based list indexes. * * Supported inputs: * - `0_1_2` * - `1.2.3` * - arrays of integers * * @param {string|Array|null} pathValue Candidate path value. * @returns {number[]|null} Parsed zero-based indexes. */ function normalizePathIndexes(pathValue) { if (Array.isArray(pathValue)) { const indexes = pathValue.map(value => Number.parseInt(value, 10)); return indexes.every(Number.isInteger) && indexes.every(index => index >= 0) ? indexes : null; } const raw = typeof pathValue === 'string' ? pathValue.trim() : ''; if (!raw) { return []; } const separator = raw.includes('.') ? '.' : '_'; const parts = raw.split(separator).filter(Boolean); if (!parts.length) { return []; } const indexes = parts.map(part => Number.parseInt(part, 10)); if (!indexes.every(Number.isInteger)) { return null; } if (separator === '.') { return indexes.every(index => index > 0) ? indexes.map(index => index - 1) : null; } return indexes.every(index => index >= 0) ? indexes : null; } /** * Convert one zero-based path index list into public path metadata. * * @param {number[]} indexes Zero-based indexes. * @returns {{path: string, pathLabel: string, depth: number}} Path metadata. */ function buildPathMeta(indexes) { const safeIndexes = Array.isArray(indexes) ? indexes.filter(Number.isInteger) : []; return { path: safeIndexes.join('_'), pathLabel: safeIndexes.map(index => index + 1).join('.'), depth: Math.max(0, safeIndexes.length - 1), }; } /** * Return the direct child list element for one list item. * * @param {HTMLLIElement|null} listItem Candidate list item. * @returns {HTMLElement|null} Direct nested list, if present. */ function getDirectChildList(listItem) { if (!listItem || listItem.nodeType !== Node.ELEMENT_NODE || listItem.tagName !== 'LI') { return null; } return Array.from(listItem.children || []).find(child => ( child.tagName === 'UL' || child.tagName === 'OL' )) || null; } /** * Return the preferred nested list tag for one list item. * * @param {Object|null} tracker Active list tracker. * @param {HTMLLIElement} listItem Parent list item. * @returns {string} `UL` or `OL`. */ function getPreferredChildListTagName(tracker, listItem) { const directChildList = getDirectChildList(listItem); if (directChildList) { return directChildList.tagName; } const parentList = listItem?.parentElement; if (parentList && (parentList.tagName === 'UL' || parentList.tagName === 'OL')) { return parentList.tagName; } return tracker?.listElement?.tagName === 'OL' ? 'OL' : 'UL'; } /** * Return one direct child list for a list item, creating it only when needed. * * Outdent can promote one item and then re-home its trailing siblings beneath * that promoted item. When the promoted item already owns a child list, those * siblings must be appended into the existing list so the DOM mirrors native * editor behavior instead of creating duplicate sibling list wrappers. * * @param {Object|null} tracker Active list tracker. * @param {HTMLLIElement} listItem Parent list item. * @param {string} preferredTagName Fallback list tag name. * @returns {HTMLElement|null} Direct child list element. */ function ensureDirectChildList(tracker, listItem, preferredTagName = '') { if (!listItem || listItem.nodeType !== Node.ELEMENT_NODE || listItem.tagName !== 'LI') { return null; } let childList = getDirectChildList(listItem); if (childList) { return childList; } childList = document.createElement( preferredTagName || getPreferredChildListTagName(tracker, listItem) ); childList.classList.add('wp-block-list'); listItem.appendChild(childList); return childList; } /** * Remove empty nested list wrappers up the ancestry chain. * * The root list element is never removed, even when it becomes empty. * * @param {Object|null} tracker Active list tracker. * @param {HTMLElement|null} startList First candidate nested list. * @returns {void} */ function cleanupEmptyAncestorLists(tracker, startList) { let currentList = startList; while ( currentList && currentList !== tracker?.listElement && currentList.nodeType === Node.ELEMENT_NODE && (currentList.tagName === 'UL' || currentList.tagName === 'OL') && !getDirectListItems(currentList).length ) { const parentItem = currentList.parentElement?.tagName === 'LI' ? currentList.parentElement : null; currentList.remove(); currentList = parentItem ? parentItem.parentElement : null; } } /** * Build one new list item element from a structural operation payload. * * @param {Object|null} operation Candidate operation payload. * @returns {HTMLLIElement} New list item element. */ function buildListItemFromOperation(operation) { const li = document.createElement('li'); const directSurface = ensureDirectItemTextSurface(li); const runtimeUuid = normalizeRuntimeUuid( operation?.itemUuid ?? operation?.newItemUuid ); const html = typeof operation?.contentHtml === 'string' ? operation.contentHtml : (typeof operation?.html === 'string' ? operation.html : ''); const text = typeof operation?.contentText === 'string' ? operation.contentText : (typeof operation?.text === 'string' ? operation.text : ''); if (html) { directSurface.innerHTML = html; } else if (text) { directSurface.textContent = text; } if (runtimeUuid) { li.setAttribute(LIST_ITEM_RUNTIME_UUID_ATTR, runtimeUuid); } return li; } /** * Copy one donor item's structural/style attributes onto the destination list * item while preserving the destination runtime UUID. * * Native Enter list splitting happens inside the browser's contenteditable * engine, so the new sibling inherits the source `li` element's attributes * such as class and style automatically. API-driven insert/move operations * should mirror that behavior by cloning the donor item's `li` attributes, * except for the session-scoped runtime UUID which must stay unique. * * @param {HTMLLIElement|null} listItem Destination list item. * @param {HTMLLIElement|null} donorItem Style/structure donor item. * @returns {void} */ function copyDonorItemAttributes(listItem, donorItem) { if ( !listItem || listItem.nodeType !== Node.ELEMENT_NODE || listItem.tagName !== 'LI' || !donorItem || donorItem.nodeType !== Node.ELEMENT_NODE || donorItem.tagName !== 'LI' ) { return; } const destinationRuntimeUuid = normalizeRuntimeUuid( listItem.getAttribute(LIST_ITEM_RUNTIME_UUID_ATTR) ); Array.from(listItem.attributes || []).forEach(attr => { if (attr?.name === LIST_ITEM_RUNTIME_UUID_ATTR) { return; } listItem.removeAttribute(attr.name); }); Array.from(donorItem.attributes || []).forEach(attr => { if (attr?.name === LIST_ITEM_RUNTIME_UUID_ATTR) { return; } listItem.setAttribute(attr.name, attr.value); }); if (destinationRuntimeUuid) { listItem.setAttribute(LIST_ITEM_RUNTIME_UUID_ATTR, destinationRuntimeUuid); } } /** * Reassign one list item's structural ID from its destination styling * context. * * When an explicit target item is known, its structural `data-item-id` * becomes the style donor for insert-before, insert-after, move-before, and * move-after commands. This keeps the inheritance rule simple and matches the * public API's explicit `targetItemUuid` model. * * For internal list-path insert/move cases that do not resolve through one * target item, fall back to the local destination neighbors. If there is no * neighboring item at all, clear the structural ID so the next tracker rebuild * seeds a fresh item identity instead of accidentally preserving source attrs. * * @param {HTMLLIElement|null} listItem Destination list item. * @param {HTMLLIElement|null} targetItem Explicit style donor item. * @returns {string} Applied structural ID or an empty string. */ function inheritDestinationItemId(listItem, targetItem = null) { if (!listItem || listItem.nodeType !== Node.ELEMENT_NODE || listItem.tagName !== 'LI') { return ''; } const explicitTargetItem = targetItem && targetItem.nodeType === Node.ELEMENT_NODE && targetItem.tagName === 'LI' ? targetItem : null; const previousItem = listItem.previousElementSibling?.tagName === 'LI' ? listItem.previousElementSibling : null; const nextItem = listItem.nextElementSibling?.tagName === 'LI' ? listItem.nextElementSibling : null; const inheritedId = normalizeRuntimeUuid( explicitTargetItem?.getAttribute(LIST_ITEM_ID_ATTR) || previousItem?.getAttribute(LIST_ITEM_ID_ATTR) || nextItem?.getAttribute(LIST_ITEM_ID_ATTR) ); if (inheritedId) { listItem.setAttribute(LIST_ITEM_ID_ATTR, inheritedId); return inheritedId; } listItem.removeAttribute(LIST_ITEM_ID_ATTR); return ''; } /** * Apply one remove-list-item operation. * * @param {Object|null} tracker Active list tracker. * @param {HTMLLIElement} listItem Target list item. * @returns {boolean} True when the mutation applied. */ function applyRemoveListItemOperation(tracker, listItem) { if (!tracker?.listElement || !listItem) { return false; } const oldParentList = listItem.parentElement; listItem.remove(); cleanupEmptyAncestorLists(tracker, oldParentList); return true; } /** * Apply one indent-list-item operation. * * @param {Object|null} tracker Active list tracker. * @param {HTMLLIElement} listItem Target list item. * @returns {boolean} True when the mutation applied. */ function applyIndentListItemOperation(tracker, listItem) { if (!tracker?.listElement || !listItem) { return false; } const previousItem = listItem.previousElementSibling?.tagName === 'LI' ? listItem.previousElementSibling : null; if (!previousItem) { return false; } const nestedList = ensureDirectChildList(tracker, previousItem); nestedList.appendChild(listItem); return true; } /** * Apply one outdent-list-item operation. * * @param {Object|null} tracker Active list tracker. * @param {HTMLLIElement} listItem Target list item. * @returns {boolean} True when the mutation applied. */ function applyOutdentListItemOperation(tracker, listItem) { if (!tracker?.listElement || !listItem) { return false; } const parentList = listItem.parentElement; const parentItem = parentList?.parentElement?.tagName === 'LI' ? parentList.parentElement : null; if (!parentList || !parentItem) { return false; } const ancestorList = parentItem.parentElement; const followingSiblings = []; let next = listItem.nextElementSibling; while (next) { followingSiblings.push(next); next = next.nextElementSibling; } ancestorList.insertBefore(listItem, parentItem.nextElementSibling); if (followingSiblings.length) { const nestedList = ensureDirectChildList(tracker, listItem, parentList.tagName); followingSiblings.forEach(sibling => nestedList.appendChild(sibling)); } cleanupEmptyAncestorLists(tracker, parentList); return true; } /** * Apply one toggle-list-type operation. * * @param {Object|null} tracker Active list tracker. * @param {Object} rawOperation Structural operation payload. * @param {Object} options Apply-operation options. * @returns {boolean} True when the mutation applied. */ function applyToggleListTypeOperation(tracker, rawOperation, options = {}) { if (!tracker?.listElement) { return false; } const listPath = rawOperation.listPath ?? rawOperation.list_path ?? ''; const targetList = getListByPath(tracker, listPath); const editorHost = options?.editorHost && typeof options.editorHost.changeListType === 'function' ? options.editorHost : null; const requestedType = targetList ? ( normalizeListTypeTagName( rawOperation.value ?? rawOperation.listType ?? rawOperation.list_type ?? rawOperation.ordered ) || (targetList.tagName === 'OL' ? 'UL' : 'OL') ) : ''; if ( !targetList || !requestedType || targetList.tagName === requestedType || !editorHost ) { return false; } const nextList = editorHost.changeListType(targetList, requestedType.toLowerCase(), { saveHistory: options.saveHistory !== false, restoreCursor: options.restoreCursor !== false, }); return !!nextList; } /** * Apply one update-list-item-text operation. * * @param {HTMLLIElement} listItem Target list item. * @param {Object} rawOperation Structural operation payload. * @returns {boolean} True when the mutation applied. */ function applyUpdateListItemTextOperation(listItem, rawOperation) { if (!listItem) { return false; } const directSurface = ensureDirectItemTextSurface(listItem); const html = typeof rawOperation?.contentHtml === 'string' ? rawOperation.contentHtml : (typeof rawOperation?.html === 'string' ? rawOperation.html : ''); const text = typeof rawOperation?.contentText === 'string' ? rawOperation.contentText : (typeof rawOperation?.text === 'string' ? rawOperation.text : ''); if (!directSurface) { return false; } directSurface.innerHTML = ''; if (html) { directSurface.innerHTML = html; } else if (text) { directSurface.textContent = text; } return true; } /** * Insert one new list item relative to explicit before/after/list anchors. * * @param {Object|null} tracker Active list tracker. * @param {Object} rawOperation Structural operation payload. * @param {Object} trackerApi List tracker API surface. * @returns {boolean} True when the mutation applied. */ function applyInsertListItemOperation(tracker, rawOperation, trackerApi) { if (!tracker?.listElement || !trackerApi) { return false; } const beforePath = rawOperation.beforePath ?? rawOperation.before_path ?? ''; const afterPath = rawOperation.afterPath ?? rawOperation.after_path ?? ''; const listPath = rawOperation.listPath ?? rawOperation.list_path ?? ''; const beforeItem = trackerApi.getItemByPath(tracker, beforePath); const afterItem = trackerApi.getItemByPath(tracker, afterPath); const newListItem = buildListItemFromOperation(rawOperation); if (beforeItem?.parentElement) { beforeItem.parentElement.insertBefore(newListItem, beforeItem); copyDonorItemAttributes(newListItem, beforeItem); inheritDestinationItemId(newListItem, beforeItem); return true; } if (afterItem?.parentElement) { afterItem.parentElement.insertBefore(newListItem, afterItem.nextElementSibling); copyDonorItemAttributes(newListItem, afterItem); inheritDestinationItemId(newListItem, afterItem); return true; } const targetList = getListByPath(tracker, listPath); if (!targetList) { return false; } const position = typeof rawOperation.position === 'string' ? rawOperation.position.trim().toLowerCase() : 'append'; if (position === 'prepend' && targetList.firstElementChild) { targetList.insertBefore(newListItem, targetList.firstElementChild); } else { targetList.appendChild(newListItem); } inheritDestinationItemId(newListItem); return true; } /** * Move one existing list item relative to explicit before/after/list anchors. * * @param {Object|null} tracker Active list tracker. * @param {HTMLLIElement} listItem Target list item. * @param {Object} rawOperation Structural operation payload. * @param {Object} trackerApi List tracker API surface. * @returns {boolean} True when the mutation applied. */ function applyMoveListItemOperation(tracker, listItem, rawOperation, trackerApi) { if (!tracker?.listElement || !listItem || !trackerApi) { return false; } const beforePath = rawOperation.beforePath ?? rawOperation.before_path ?? ''; const afterPath = rawOperation.afterPath ?? rawOperation.after_path ?? ''; const listPath = rawOperation.listPath ?? rawOperation.list_path ?? ''; const beforeItem = trackerApi.getItemByPath(tracker, beforePath); const afterItem = trackerApi.getItemByPath(tracker, afterPath); const oldParentList = listItem.parentElement; let didApply = false; if (beforeItem && beforeItem !== listItem && !listItem.contains(beforeItem)) { beforeItem.parentElement.insertBefore(listItem, beforeItem); copyDonorItemAttributes(listItem, beforeItem); inheritDestinationItemId(listItem, beforeItem); didApply = true; } else if (afterItem && afterItem !== listItem && !listItem.contains(afterItem)) { afterItem.parentElement.insertBefore(listItem, afterItem.nextElementSibling); copyDonorItemAttributes(listItem, afterItem); inheritDestinationItemId(listItem, afterItem); didApply = true; } else if (typeof listPath === 'string') { const targetList = getListByPath(tracker, listPath); const ownerItem = targetList?.parentElement?.tagName === 'LI' ? targetList.parentElement : null; if (targetList && ownerItem !== listItem && !listItem.contains(ownerItem || null)) { const position = typeof rawOperation.position === 'string' ? rawOperation.position.trim().toLowerCase() : 'append'; if (position === 'prepend' && targetList.firstElementChild) { targetList.insertBefore(listItem, targetList.firstElementChild); } else { targetList.appendChild(listItem); } inheritDestinationItemId(listItem); didApply = true; } } if (didApply) { cleanupEmptyAncestorLists(tracker, oldParentList); } return didApply; } /** * Resolve one tracked list element from a list-path payload. * * The root list lives at the empty path. Nested lists are addressed by the * tree path of the parent item that owns that child list. * * @param {Object|null} tracker Active list tracker. * @param {string|Array} pathValue Root-empty list path or parent-item path. * @returns {HTMLElement|null} Matching list element. */ function getListByPath(tracker, pathValue) { if (!tracker?.listElement) { return null; } if ( pathValue === '' || pathValue === null || typeof pathValue === 'undefined' || (Array.isArray(pathValue) && !pathValue.length) || (typeof pathValue === 'string' && !pathValue.trim()) ) { return tracker.listElement; } const parentItem = ListBlockTracker.getItemByPath(tracker, pathValue); return parentItem ? getDirectChildList(parentItem) : null; } /** * Normalize one requested list-type value to a DOM tag name. * * @param {*} value Candidate list-type value. * @returns {string} `OL`, `UL`, or an empty string. */ function normalizeListTypeTagName(value) { if (value === true) { return 'OL'; } if (value === false) { return 'UL'; } const normalized = String(value || '').trim().toLowerCase(); if (normalized === 'ordered' || normalized === 'ol' || normalized === 'true') { return 'OL'; } if (normalized === 'unordered' || normalized === 'ul' || normalized === 'false') { return 'UL'; } return ''; } const ListBlockTracker = { active: null, /** * Initialize tracker for a list element * @param {HTMLElement} listElement - The UL or OL element * @param {Object} originalBlock - The complete WordPress block structure */ init(listElement, originalBlock = {}) { if (!createBlock || !serializeBlocks) { console.error('wp.blocks not available'); } const tracker = { listElement, originalBlock: JSON.parse(JSON.stringify(originalBlock)), uuidMap: new Map(), // uuid -> {type, attrs, element} domMap: new WeakMap(), // element -> uuid runtimeUuidMap: new Map(), // runtimeUuid -> {type, element} runtimeDomMap: new WeakMap() // element -> runtimeUuid }; // Attach tracker to element to avoid singleton issues listElement._mwpListTracker = tracker; // Build UUID tracking from DOM and original block structure this.buildFromDOM(tracker, listElement, originalBlock); this.active = tracker; return tracker; }, /** * Build UUID mappings from DOM and original block structure * Assigns UUIDs to all lists and list items, mapping to their original attrs */ buildFromDOM(tracker, listElement, originalBlock) { const previousEntries = tracker.uuidMap instanceof Map ? new Map(tracker.uuidMap) : new Map(); const previousRuntimeEntries = tracker.runtimeUuidMap instanceof Map ? new Map(tracker.runtimeUuidMap) : new Map(); tracker.uuidMap.clear(); tracker.domMap = new WeakMap(); tracker.runtimeUuidMap.clear(); tracker.runtimeDomMap = new WeakMap(); tracker.listElement = listElement; this.syncEditableTextSurfaces(listElement); this.registerRuntimeIdentity( tracker, listElement, 'list', previousRuntimeEntries ); // Assign UUID to root list and map to original attrs const rootUuid = this.getOrCreateUuid(listElement, 'list'); const previousRootEntry = previousEntries.get(rootUuid); tracker.uuidMap.set(rootUuid, { type: 'list', attrs: previousRootEntry?.attrs ? cloneListAttrs(previousRootEntry.attrs, true) : cloneListAttrs(originalBlock.attrs || {}, true), element: listElement }); tracker.domMap.set(listElement, rootUuid); // Recursively process list structure this.processListRecursive( tracker, listElement, originalBlock.innerBlocks || [], previousEntries, previousRuntimeEntries ); }, /** * Recursively process list items and nested lists, assigning UUIDs */ processListRecursive( tracker, listElement, originalItems, previousEntries = new Map(), previousRuntimeEntries = new Map() ) { const items = getDirectListItems(listElement); items.forEach((li, index) => { this.syncEditableTextSurfaces(li); const originalItem = originalItems[index] || {}; this.registerRuntimeIdentity( tracker, li, 'item', previousRuntimeEntries ); const itemUuid = this.getOrCreateUuid(li, 'item'); const previousItemEntry = previousEntries.get(itemUuid); // Map UUID to original item attrs tracker.uuidMap.set(itemUuid, { type: 'item', attrs: previousItemEntry?.attrs ? JSON.parse(JSON.stringify(previousItemEntry.attrs || {})) : JSON.parse(JSON.stringify(originalItem.attrs || {})), element: li }); tracker.domMap.set(li, itemUuid); // Handle nested lists const nestedList = getDirectChildList(li); if (nestedList) { const originalNested = originalItem.innerBlocks?.[0] || {}; this.registerRuntimeIdentity( tracker, nestedList, 'list', previousRuntimeEntries ); const nestedUuid = this.getOrCreateUuid(nestedList, 'list'); const previousNestedEntry = previousEntries.get(nestedUuid); // Map nested list attrs without plugin UUID ownership. // The tracker still needs a temporary DOM identity for list // editing, but persisting mwpSfeUuid* on nested lists would // fracture one logical list into multiple save/history roots. tracker.uuidMap.set(nestedUuid, { type: 'list', attrs: previousNestedEntry?.attrs ? cloneListAttrs(previousNestedEntry.attrs, false) : cloneListAttrs(originalNested.attrs || {}, false), element: nestedList }); tracker.domMap.set(nestedList, nestedUuid); // Recurse into nested list this.processListRecursive( tracker, nestedList, originalNested.innerBlocks || [], previousEntries, previousRuntimeEntries ); } }); }, /** * Register one list or list-item runtime identity on the tracker. * * @param {Object} tracker Active list tracker. * @param {Element} element Live list or list-item element. * @param {string} type Supported runtime identity type. * @param {Map} previousRuntimeEntries Previous runtime entry map. * @returns {string} Resolved runtime UUID. */ registerRuntimeIdentity( tracker, element, type, previousRuntimeEntries = new Map() ) { const runtimeUuid = this.getOrCreateRuntimeUuid( tracker, element, type, previousRuntimeEntries ); if (!runtimeUuid) { return ''; } tracker.runtimeUuidMap.set(runtimeUuid, { type, element, }); tracker.runtimeDomMap.set(element, runtimeUuid); return runtimeUuid; }, /** * Return whether one runtime UUID is already owned by a different element. * * Native contenteditable list splitting can clone DOM attributes from the * source item into the newly created sibling. When that happens, the new * runtime UUID must be reseeded so each live cursor target stays unique. * * @param {Object} tracker Active list tracker. * @param {string} runtimeUuid Candidate runtime UUID. * @param {Element|null} element Element requesting that UUID. * @returns {boolean} True when the UUID belongs elsewhere. */ isRuntimeUuidClaimedByDifferentElement(tracker, runtimeUuid, element) { const normalizedRuntimeUuid = normalizeRuntimeUuid(runtimeUuid); if (!tracker?.runtimeUuidMap || !normalizedRuntimeUuid) { return false; } const existingEntry = tracker.runtimeUuidMap.get(normalizedRuntimeUuid); return !!(existingEntry?.element && existingEntry.element !== element); }, /** * Get the inherited structural ID for an element or seed it from the * runtime UUID when it does not exist yet. * * Structural IDs may be intentionally copied by native list splitting so * related items can retain style inheritance. They are not treated as * unique runtime cursor identifiers. */ getOrCreateUuid(element, type) { const attrName = type === 'list' ? LIST_ID_ATTR : LIST_ITEM_ID_ATTR; let uuid = element.getAttribute(attrName); if (!uuid) { uuid = normalizeRuntimeUuid( element.getAttribute( type === 'list' ? LIST_RUNTIME_UUID_ATTR : LIST_ITEM_RUNTIME_UUID_ATTR ) ) || this.generateTempUuid(); element.setAttribute(attrName, uuid); } return uuid; }, /** * Return the existing runtime UUID for one element or create one. * * Caller-supplied runtime UUIDs win for newly created items/lists. When an * element already belongs to the previous tracker build, its existing * runtime UUID is preserved so API references remain stable across rebuilds. * * @param {Object} tracker Active list tracker. * @param {Element} element Live list or list-item element. * @param {string} type Supported runtime identity type. * @param {Map} previousRuntimeEntries Previous runtime entry map. * @returns {string} Session-scoped runtime UUID. */ getOrCreateRuntimeUuid( tracker, element, type, previousRuntimeEntries = new Map() ) { const attrName = getRuntimeUuidAttributeName(type); if (!attrName || !isRuntimeIdentityElement(element, type)) { return ''; } let runtimeUuid = normalizeRuntimeUuid(element.getAttribute(attrName)); if (!runtimeUuid) { for (const [candidateUuid, entry] of previousRuntimeEntries.entries()) { if (entry?.element === element && entry.type === type) { runtimeUuid = normalizeRuntimeUuid(candidateUuid); break; } } } if (this.isRuntimeUuidClaimedByDifferentElement(tracker, runtimeUuid, element)) { runtimeUuid = ''; } if (!runtimeUuid) { runtimeUuid = this.generateTempUuid(); } element.setAttribute(attrName, runtimeUuid); return runtimeUuid; }, /** * Generate a RFC4122 version 4 UUID */ generateTempUuid() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); }, /** * Return the zero-based tree path for one list item inside the tracked list. * * @param {Object} tracker Active list tracker. * @param {HTMLLIElement} listItem Candidate list item. * @returns {number[]|null} Zero-based indexes, or null on failure. */ getPathIndexesForItem(tracker, listItem) { if ( !tracker?.listElement || !listItem || listItem.nodeType !== Node.ELEMENT_NODE || listItem.tagName !== 'LI' || !tracker.listElement.contains(listItem) ) { return null; } const indexes = []; let currentItem = listItem; while (currentItem && tracker.listElement.contains(currentItem)) { const parentList = currentItem.parentElement; if (!parentList || (parentList.tagName !== 'UL' && parentList.tagName !== 'OL')) { return null; } const siblingItems = getDirectListItems(parentList); const itemIndex = siblingItems.indexOf(currentItem); if (itemIndex < 0) { return null; } indexes.unshift(itemIndex); const nextItem = parentList.closest('li'); if (!nextItem || !tracker.listElement.contains(nextItem)) { break; } currentItem = nextItem; } return indexes.length ? indexes : null; }, /** * Return the direct list item currently living at one tree path. * * @param {Object} tracker Active list tracker. * @param {string|Array} pathValue Zero-based path value. * @returns {HTMLLIElement|null} Matching list item. */ getItemByPath(tracker, pathValue) { const indexes = normalizePathIndexes(pathValue); if (!tracker?.listElement || !Array.isArray(indexes) || !indexes.length) { return null; } let currentList = tracker.listElement; let currentItem = null; for (let index = 0; index < indexes.length; index++) { const itemIndex = indexes[index]; const siblingItems = getDirectListItems(currentList); currentItem = siblingItems[itemIndex] || null; if (!currentItem) { return null; } if (index === indexes.length - 1) { return currentItem; } currentList = getDirectChildList(currentItem); if (!currentList) { return null; } } return currentItem; }, /** * Resolve one list element back to its public list path. * * The root list is addressed as an empty string. Nested child lists are * addressed by the path of the parent list item that owns them. * * @param {Object|null} tracker Active list tracker. * @param {HTMLElement|null} listElement Candidate list element. * @returns {string} Root-relative list path. */ getPathForList(tracker, listElement) { if (!tracker) tracker = this.active; if (!tracker?.listElement || !listElement || listElement.nodeType !== Node.ELEMENT_NODE) { return ''; } if (listElement === tracker.listElement) { return ''; } const parentItem = ( listElement.parentElement && listElement.parentElement.tagName === 'LI' ) ? listElement.parentElement : null; if (!parentItem) { return ''; } const indexes = this.getPathIndexesForItem(tracker, parentItem); return Array.isArray(indexes) ? indexes.join('_') : ''; }, /** * Return one tracked list item by runtime UUID. * * @param {Object|null} tracker Active list tracker. * @param {string} runtimeUuid Session-scoped item UUID. * @returns {HTMLLIElement|null} Matching list item. */ getItemByRuntimeUuid(tracker, runtimeUuid) { if (!tracker) tracker = this.active; const element = findElementByRuntimeUuid(tracker, runtimeUuid, 'item'); return element && element.tagName === 'LI' ? element : null; }, /** * Return one tracked list by runtime UUID. * * @param {Object|null} tracker Active list tracker. * @param {string} runtimeUuid Session-scoped list UUID. * @returns {HTMLElement|null} Matching list element. */ getListByRuntimeUuid(tracker, runtimeUuid) { if (!tracker) tracker = this.active; const element = findElementByRuntimeUuid(tracker, runtimeUuid, 'list'); return isListElementForRuntimeLookup(element) ? element : null; }, /** * Return the runtime UUID for one tracked list item. * * @param {Object|null} tracker Active list tracker. * @param {HTMLLIElement} listItem Live list item. * @returns {string} Session-scoped item UUID. */ getRuntimeUuidForItem(tracker, listItem) { if (!tracker) tracker = this.active; if (!tracker?.listElement || !listItem || listItem.tagName !== 'LI') { return ''; } return normalizeRuntimeUuid( listItem.getAttribute(LIST_ITEM_RUNTIME_UUID_ATTR) || tracker.runtimeDomMap?.get(listItem) ); }, /** * Return the runtime UUID for one tracked list. * * @param {Object|null} tracker Active list tracker. * @param {HTMLElement} listElement Live list element. * @returns {string} Session-scoped list UUID. */ getRuntimeUuidForList(tracker, listElement) { if (!tracker) tracker = this.active; if (!tracker?.listElement || !isListElementForRuntimeLookup(listElement)) { return ''; } return normalizeRuntimeUuid( listElement.getAttribute(LIST_RUNTIME_UUID_ATTR) || tracker.runtimeDomMap?.get(listElement) ); }, /** * Parse current DOM list structure to WordPress block format * Uses UUID to preserve original attrs, updates only content and ordered * * @param {HTMLElement} listElement Live UL/OL element. * @param {Object} tracker Active list tracker. * @param {boolean} allowUuidOwnership True only for the outermost * core/list. Nested lists must * serialize without persisted * plugin UUID attrs. */ parseListToBlock(listElement, tracker, allowUuidOwnership = true) { const isOrdered = listElement.tagName === 'OL'; const uuid = listElement.getAttribute(LIST_ID_ATTR); let attrs = { ordered: isOrdered }; if (uuid && tracker && tracker.uuidMap.has(uuid)) { const original = tracker.uuidMap.get(uuid); const originalAttrs = (Array.isArray(original.attrs) || !original.attrs) ? {} : cloneListAttrs(original.attrs, allowUuidOwnership); attrs = { ...originalAttrs, ordered: isOrdered }; } const listItems = Array.from(listElement.children).filter(el => el.tagName === 'LI'); const innerBlocks = listItems.map(li => this.parseListItemToBlock(li, tracker)); return createBlock('core/list', attrs, innerBlocks); }, /** * Parse a list item to WordPress block format * Preserves original attrs if UUID exists */ parseListItemToBlock(liElement, tracker) { const uuid = liElement.getAttribute(LIST_ITEM_ID_ATTR); let attrs = {}; if (uuid && tracker && tracker.uuidMap.has(uuid)) { const original = tracker.uuidMap.get(uuid); attrs = JSON.parse(JSON.stringify(original.attrs || {})); } // Recurse into nested list using the live element before cloning const innerBlocks = []; const nestedListEl = liElement.querySelector(':scope > ul, :scope > ol'); if (nestedListEl) { innerBlocks.push(this.parseListToBlock(nestedListEl, tracker, false)); } const clone = liElement.cloneNode(true); const directSurface = clone.querySelector(`:scope > [${LIST_ITEM_TEXT_ATTR}="1"]`); let content = ''; if (directSurface) { const cleanedSurface = this.cleanElement(directSurface); content = cleanedSurface.innerHTML.trim(); } else { const nestedInClone = clone.querySelector(':scope > ul, :scope > ol'); if (nestedInClone) nestedInClone.remove(); const cleaned = this.cleanElement(clone); content = cleaned.innerHTML.trim(); } return createBlock('core/list-item', { ...attrs, content }, innerBlocks); }, /** * Clean element using ElementPrep * Removes editing artifacts while preserving UUIDs and content */ cleanElement(element) { if (SFE.ElementPrep) { return SFE.ElementPrep.clean(element, { removeIdentity: true, removeControls: true, clone: false // Already working with a clone }); } console.error('ElementPrep not found'); return element; }, /** * Serialize the current list structure to WordPress block format * Preserves all original attrs, updates only content and ordered */ serialize(tracker) { if (!tracker) tracker = this.active; if (!tracker) return null; const block = this.parseListToBlock(tracker.listElement, tracker); return serializeBlocks([block]); }, /** * Update the tracker's element reference * Used when the list element is replaced (e.g., OL to UL conversion) */ updateElement(tracker, newElement) { if (!tracker) tracker = this.active; if (!tracker) return; // Ensure new element keeps the tracker reference newElement._mwpListTracker = tracker; this.syncEditableTextSurfaces(newElement); tracker.listElement = newElement; // Update element references in uuidMap tracker.uuidMap.forEach((value, key) => { if (value.type === 'list') { const newEl = newElement.querySelector(`[${LIST_ID_ATTR}="${key}"]`) || (newElement.getAttribute(LIST_ID_ATTR) === key ? newElement : null); if (newEl) { value.element = newEl; } } else if (value.type === 'item') { const newEl = newElement.querySelector(`[${LIST_ITEM_ID_ATTR}="${key}"]`); if (newEl) { value.element = newEl; } } }); tracker.runtimeUuidMap.forEach((value, key) => { if (value.type === 'list') { const newEl = this.getListByRuntimeUuid(tracker, key); if (newEl) { value.element = newEl; tracker.runtimeDomMap.set(newEl, key); } } else if (value.type === 'item') { const newEl = this.getItemByRuntimeUuid(tracker, key); if (newEl) { value.element = newEl; tracker.runtimeDomMap.set(newEl, key); } } }); }, /** * Return one tree-aware structural snapshot for the active list. * * @param {Object|null} tracker Active list tracker. * @returns {Object|null} Lightweight structure snapshot. */ getStructure(tracker) { if (!tracker) tracker = this.active; if (!tracker?.listElement) { return null; } const buildListNode = (listElement, listPath = '', parentIndexes = []) => ({ listUuid: this.getRuntimeUuidForList(tracker, listElement), listPath, ordered: listElement.tagName === 'OL', items: getDirectListItems(listElement).map((listItem, index) => { const indexes = [ ...parentIndexes, index ]; const pathMeta = buildPathMeta(indexes); const directSurface = ensureDirectItemTextSurface(listItem); const nestedList = getDirectChildList(listItem); return { itemUuid: this.getRuntimeUuidForItem(tracker, listItem), path: pathMeta.path, pathLabel: pathMeta.pathLabel, depth: pathMeta.depth, contentHtml: directSurface ? directSurface.innerHTML.trim() : '', childList: nestedList ? buildListNode(nestedList, pathMeta.path, indexes) : null, }; }), }); return buildListNode(tracker.listElement, '', []); }, /** * Apply one primitive structural list operation against the live tracked * DOM. * * Public API calls are translated into this lower-level operation set by * the shared schema executor so the tracker only needs to understand the * canonical primitive mutation layer. * * Supported primitive kinds: * - `insert_list_item` * - `remove_list_item` * - `move_list_item` * - `indent_list_item` * - `outdent_list_item` * - `update_list_item_text` * - `toggle_list_type` * * @param {Object|null} tracker Active list tracker. * @param {Object} rawOperation Structural operation payload. * @returns {Object|null} Result summary when applied. */ applyOperation(tracker, rawOperation, options = {}) { if (!tracker) tracker = this.active; if (!tracker?.listElement || !rawOperation || typeof rawOperation !== 'object') { return null; } const kind = typeof rawOperation.kind === 'string' ? rawOperation.kind.trim().toLowerCase() : ''; const path = rawOperation.path ?? rawOperation.itemPath ?? rawOperation.item_path ?? ''; const listItem = this.getItemByPath(tracker, path); let didApply = false; if (kind === 'remove_list_item' && listItem) { didApply = applyRemoveListItemOperation(tracker, listItem); } else if (kind === 'indent_list_item' && listItem) { didApply = applyIndentListItemOperation(tracker, listItem); } else if (kind === 'outdent_list_item' && listItem) { didApply = applyOutdentListItemOperation(tracker, listItem); } else if (kind === 'toggle_list_type') { didApply = applyToggleListTypeOperation(tracker, rawOperation, options); } else if (kind === 'update_list_item_text' && listItem) { didApply = applyUpdateListItemTextOperation(listItem, rawOperation); } else if (kind === 'insert_list_item') { didApply = applyInsertListItemOperation(tracker, rawOperation, this); } else if (kind === 'move_list_item' && listItem) { didApply = applyMoveListItemOperation(tracker, listItem, rawOperation, this); } if (!didApply) { return null; } this.syncEditableTextSurfaces(tracker.listElement); this.buildFromDOM(tracker, tracker.listElement, tracker.originalBlock || {}); return { kind, structure: this.getStructure(tracker), }; }, /** * Ensure every list item in one tree has one direct text surface. * * @param {HTMLElement|null} rootElement Candidate list root or list item. * @returns {HTMLElement|null} Normalized root element. */ syncEditableTextSurfaces(rootElement) { if (!rootElement || rootElement.nodeType !== Node.ELEMENT_NODE) { return null; } if (rootElement.tagName === 'LI') { ensureDirectItemTextSurface(rootElement); Array.from(rootElement.children || []) .filter(child => isNestedListNode(child)) .forEach(childList => this.syncEditableTextSurfaces(childList)); return rootElement; } if (rootElement.tagName !== 'UL' && rootElement.tagName !== 'OL') { return rootElement; } Array.from(rootElement.children || []) .filter(child => child.tagName === 'LI') .forEach(li => this.syncEditableTextSurfaces(li)); return rootElement; }, /** * Ensure runtime UUID attributes are unique within one live list tree. * * Native contenteditable list splitting can clone `li` and nested list * attributes verbatim before FrontEdit regains control. This pass reseeds only the * session-scoped runtime UUID attributes so freshly created structures become * distinct live API/editor targets immediately, while leaving `data-item-id` * and `data-list-id` untouched for their separate responsibilities. * * @param {HTMLElement|null} rootElement Candidate list root. * @returns {HTMLElement|null} Normalized root element. */ ensureUniqueRuntimeUuids(rootElement) { if ( !rootElement || rootElement.nodeType !== Node.ELEMENT_NODE || (rootElement.tagName !== 'UL' && rootElement.tagName !== 'OL') ) { return rootElement || null; } const seenListRuntimeUuids = new Set(); const seenItemRuntimeUuids = new Set(); const collectLists = [ rootElement, ...Array.from(rootElement.querySelectorAll('ul, ol')) ]; const collectItems = Array.from(rootElement.querySelectorAll('li')); collectLists.forEach(listElement => { const runtimeUuid = normalizeRuntimeUuid( listElement.getAttribute(LIST_RUNTIME_UUID_ATTR) ); if (!runtimeUuid) { return; } if (seenListRuntimeUuids.has(runtimeUuid)) { listElement.setAttribute(LIST_RUNTIME_UUID_ATTR, this.generateTempUuid()); return; } seenListRuntimeUuids.add(runtimeUuid); }); collectItems.forEach(listItem => { const runtimeUuid = normalizeRuntimeUuid( listItem.getAttribute(LIST_ITEM_RUNTIME_UUID_ATTR) ); if (!runtimeUuid) { return; } if (seenItemRuntimeUuids.has(runtimeUuid)) { listItem.setAttribute(LIST_ITEM_RUNTIME_UUID_ATTR, this.generateTempUuid()); return; } seenItemRuntimeUuids.add(runtimeUuid); }); return rootElement; }, /** * Ensure one live list tree has the required structural IDs and runtime UUIDs. * * On first editor activation, list elements may not yet have either identity * attribute family. Seed both from one generated UUID per element so the * initial history snapshot captures stable targeting data. Later native list * splits may intentionally copy the structural IDs; in those cases this pass * preserves the structural IDs and only reseeds duplicated runtime UUIDs. * * @param {HTMLElement|null} rootElement Candidate list root. * @returns {HTMLElement|null} Normalized root element. */ ensureIdentityAttributes(rootElement) { if ( !rootElement || rootElement.nodeType !== Node.ELEMENT_NODE || (rootElement.tagName !== 'UL' && rootElement.tagName !== 'OL') ) { return rootElement || null; } const syncIdentityPair = (element, structuralAttrName, runtimeAttrName) => { if (!element || element.nodeType !== Node.ELEMENT_NODE) { return; } let structuralId = normalizeRuntimeUuid( element.getAttribute(structuralAttrName) ); let runtimeUuid = normalizeRuntimeUuid( element.getAttribute(runtimeAttrName) ); if (!structuralId && !runtimeUuid) { runtimeUuid = this.generateTempUuid(); structuralId = runtimeUuid; } else if (!runtimeUuid) { runtimeUuid = structuralId; } else if (!structuralId) { structuralId = runtimeUuid; } element.setAttribute(structuralAttrName, structuralId); element.setAttribute(runtimeAttrName, runtimeUuid); }; syncIdentityPair(rootElement, LIST_ID_ATTR, LIST_RUNTIME_UUID_ATTR); Array.from(rootElement.querySelectorAll('ul, ol')).forEach(listElement => { syncIdentityPair(listElement, LIST_ID_ATTR, LIST_RUNTIME_UUID_ATTR); }); Array.from(rootElement.querySelectorAll('li')).forEach(listItem => { syncIdentityPair(listItem, LIST_ITEM_ID_ATTR, LIST_ITEM_RUNTIME_UUID_ATTR); }); return this.ensureUniqueRuntimeUuids(rootElement); }, getDirectItemTextSurface, normalizePathIndexes, buildPathMeta, getRuntimeUuidAttributeName, getListRuntimeUuidAttributeName() { return LIST_RUNTIME_UUID_ATTR; }, getListItemRuntimeUuidAttributeName() { return LIST_ITEM_RUNTIME_UUID_ATTR; }, getListIdAttributeName() { return LIST_ID_ATTR; }, getListItemIdAttributeName() { return LIST_ITEM_ID_ATTR; }, /** * Destroy tracker and clean up */ destroy(tracker) { if (!tracker) tracker = this.active; if (!tracker) return; tracker.uuidMap.clear(); tracker.runtimeUuidMap.clear(); if (tracker.listElement) { delete tracker.listElement._mwpListTracker; } if (this.active === tracker) this.active = null; } }; /** * Return whether one element is a live list node for runtime lookup helpers. * * @param {Element|null} element Candidate DOM element. * @returns {boolean} True when the element is `UL` or `OL`. */ function isListElementForRuntimeLookup(element) { return !!( element && element.nodeType === Node.ELEMENT_NODE && (element.tagName === 'UL' || element.tagName === 'OL') ); } // Expose globally SFE.ListBlockTracker = ListBlockTracker; })(); Page 3 – Алумни Универзитета у Приштини

Alumni of the University of Pristina

Универзитет и алумнисти.

Заједно.

Legjobbkaszino - Megbízható Útmutató az Online Kaszinók Világában

A Legjobbkaszino egy megbízható információs forrás mindazok számára, akik szeretnének eligazodni az online kaszinók világában és biztonságos környezetben szeretnének játszani. Az oldal részletes értékeléseket, frissített toplistákat és szakértői tanácsokat kínál, hogy a játékosok biztonságosan és tudatosan választhassák ki a számukra legmegfelelőbb szolgáltatót. A platform hitelességét a független szakértői munka, az átlátható értékelési módszertan és a folyamatos piaci monitoring garantálja, így minden magyar játékos megbízhat az itt található ajánlásokban és információkban.

Mi a Legjobbkaszino és Miért Megbízható?

A Legjobbkaszino egy szakértői csapat által működtetett portál, amely kizárólag a minőségi online kaszinók bemutatására és értékelésére specializálódott. A platform létrejötte a magyar játékosok igényeire adott válasz, mivel egyre többen keresnek megbízható forrásokat a kaszinóválasztáshoz.

A Platform Alapelvei

A Legjobbkaszino működését szigorú etikai szabályok és professzionális standardok határozzák meg. A portál elkötelezett amellett, hogy csak olyan kaszinókat ajánljon, amelyek minden szempontból megfelelnek a legmagasabb minőségi követelményeknek. Ez a következetes minőségbiztosítás teszi a Legjobbkaszinot megbízható partnerebbé a játékosok számára.

  • Függetlenség: A Legjobbkaszino értékelései teljes mértékben függetlenek és objektívek, nem befolyásolják őket kereskedelmi érdekek.
  • Átláthatóság: Minden értékelési szempont és módszertan nyilvánosan elérhető és érthető.
  • Szakértelem: A csapat tagjai évtizedes tapasztalattal rendelkeznek a szerencsejáték-iparban.
  • Naprakészség: Az információk folyamatosan frissülnek, követve a piaci változásokat.
  • Játékosvédelem: A biztonság és a felelős szerencsejáték mindig prioritást élvez.

Részletes Értékelési Rendszer

A Legjobbkaszino komplex értékelési rendszert alkalmaz, amely több mint 60 különböző szempontot vizsgál minden egyes kaszinónál. Ez a sokrétű megközelítés biztosítja, hogy csak a valóban kiváló platformok kerüljenek fel a toplistákra.

Főbb Értékelési Kategóriák

Az értékelési folyamat során a Legjobbkaszino szakértői minden lényeges aspektust megvizsgálnak. A pontozás során a különböző kategóriák eltérő súllyal bírnak, tükrözve azok fontosságát a játékosok számára.

KategóriaSúly (%)Főbb SzempontokÉrtékelés
Biztonság30%Licenc, SSL, adatvédelem1-10 skála
Játékválaszték25%Játékok száma, minőség, fejlesztők1-10 skála
Bónuszok20%Értékek, feltételek, változatosság1-10 skála
Fizetések15%Sebesség, módszerek, korlátok1-10 skála
Ügyfélszolgálat10%Elérhetőség, nyelvtudás, hatékonyság1-10 skála

Tesztelési Folyamat Lépései

A Legjobbkaszino szakértői alapos tesztelési folyamatot követnek minden új kaszinó értékelésekor. Ez a folyamat több hétig is eltarthat, és valódi pénzzel történő játékot is magában foglal, így biztosítva a hiteles visszajelzéseket.

Frissített Toplisták Különböző Kategóriákban

A Legjobbkaszino egyik legnagyobb értéke a sokrétű toplista rendszere. A platform nem egyetlen általános listát kínál, hanem több specifikus kategóriában is rangsorolja a kaszinókat, így minden játékos megtalálhatja az igényeinek legmegfelelőbb platformot.

Tematikus Rangsorok

A Legjobbkaszino különböző tematikus toplistákat vezet, amelyek specifikus játékosi preferenciákat szolgálnak ki. Ezek a listák külön-külön értékelik a kaszinókat az adott szempont szerint, így pontosabb képet adva.

  • Top 10 általános kaszinó: Az átfogóan legjobb teljesítményt nyújtó platformok.
  • Legjobb üdvözlő bónuszok: A legvonzóbb befizetési bónuszokat kínáló kaszinók.
  • Gyors kifizetésű kaszinók: 24 órán belüli kifizetést garantáló szolgáltatók.
  • Magyar nyelvű kaszinók: Teljes magyar nyelvű támogatást nyújtó platformok.
  • Mobil kaszinók: Kiváló mobilélményt biztosító oldalak.
  • Élő kaszinó specialisták: A legjobb live dealer játékokat kínáló szolgáltatók.
  • Nyerőgép paradicsomok: Több ezer nyerőgéppel rendelkező platformok.

Szakértői Tanácsok és Útmutatók

A Legjobbkaszino nem csupán kaszinókat értékel, hanem átfogó oktatási tartalmat is kínál. A platform részletes útmutatókat, stratégiákat és tippeket tesz közzé, amelyek segítenek a játékosoknak fejleszteni tudásukat és javítani eredményeiket.

Kezdőknek Szóló Útmutatók

A Legjobbkaszino különös figyelmet fordít azokra, akik most ismerkednek az online szerencsejátékkal. Ezek az útmutatók lépésről lépésre vezetik végig a játékosokat a regisztrációtól az első nyereményig.

Haladó Stratégiák

A tapasztalt játékosok számára a Legjobbkaszino fejlett stratégiákat és taktikákat mutat be különböző játékokhoz. Ezek a tartalmak segítenek maximalizálni a nyerési esélyeket és optimalizálni a bankroll kezelést.

JátéktípusAlapstratégiaHaladó TechnikaAjánlott Bankroll
NyerőgépekRTP választás, volatilitásBet sizing, session management50-100x tét
BlackjackAlapstratégia táblázatKártyaszámolás, deviációk100-200x minimum tét
RulettKülső fogadásokProgressziós rendszerek200-300x tét
BaccaratBanker fogadásMinta követés korlátai100-150x tét

Bónusz Útmutató és Összehasonlítások

A Legjobbkaszino egyik legértékesebb szolgáltatása a részletes bónuszelemzés. A platform nemcsak felsorolja a bónuszokat, hanem mélyreható analízist is nyújt róluk, beleértve a valós érték kiszámítását és a teljesíthetőség értékelését.

Bónusz Kalkulátor

A Legjobbkaszino egyedi bónusz kalkulátort fejlesztett ki, amely segít a játékosoknak kiszámítani egy bónusz valós értékét. Az eszköz figyelembe veszi a forgalmi követelményeket, a játékhozzájárulási arányokat és a maximális kifizetési limiteket.

Exkluzív Ajánlatok

A Legjobbkaszino rendszeresen tárgyal partneri kaszinókkal, hogy exkluzív bónuszokat biztosítson látogatói számára. Ezek az ajánlatok gyakran jobbak, mint amit közvetlenül a kaszinó oldalán találnánk, így érdemes a Legjobbkaszinon keresztül regisztrálni.

Fizetési Módok Részletes Áttekintése

A Legjobbkaszino átfogó információkat szolgáltat a különböző fizetési módokról. A platform részletesen bemutatja az egyes megoldások előnyeit, hátrányait, sebességét és költségeit, különös tekintettel a magyar játékosok számára releváns opciókra.

Forint Támogatás Fontossága

A Legjobbkaszino kiemelt figyelmet fordít azokra a kaszinókra, amelyek támogatják a forintban történő játékot. Ez jelentős előnyt jelent a magyar játékosok számára, mivel elkerülhetők a deviza átváltási díjak és az árfolyamkockázat.

  • Nincs átváltási díj: Forintban játszva megspóroljuk az árfolyam különbözetet.
  • Átlátható költségek: Pontosan tudjuk, mennyit fizetünk be és mennyit nyerünk.
  • Gyorsabb tranzakciók: Nincs szükség deviza átváltásra, így gyorsabb a feldolgozás.
  • Magyar banki módszerek: Helyi fizetési megoldások könnyebb használata.

Mobilkaszinózás Értékelése

A Legjobbkaszino felismeri, hogy egyre többen játszanak mobil eszközökről, ezért minden értékelés külön szekciót szentel a mobilkompatibilitásnak. A platform részletesen teszteli a kaszinók mobil verzióit és alkalmazásait.

Mobil Tesztelési Kritériumok

A mobil értékelés során a Legjobbkaszino több eszközön is teszteli a kaszinókat, beleértve különböző operációs rendszereket és képernyőméreteket. Ez biztosítja, hogy a játékosok pontos képet kapjanak a mobil élményről.

Biztonság és Felelős Játék

A Legjobbkaszino kiemelt fontosságot tulajdonít a játékosok biztonságának és a felelős szerencsejáték népszerűsítésének. Minden ajánlott kaszinónál részletesen bemutatja a biztonsági intézkedéseket és a felelős játék eszközöket.

Biztonsági Ellenőrzések

A Legjobbkaszino alaposan megvizsgálja minden kaszinó biztonsági infrastruktúráját. Csak azok a platformok kapnak ajánlást, amelyek megfelelnek a legszigorúbb biztonsági standardoknak és rendelkeznek érvényes szerencsejáték-engedéllyel.

Felelős Játék Támogatása

A Legjobbkaszino aktívan népszerűsíti a felelős szerencsejátékot. A platform részletes útmutatókat kínál az önkorlátozási eszközök használatáról, és információkat szolgáltat a segítségkérés lehetőségeiről.

Közösségi Funkciók

A Legjobbkaszino nem csak egyirányú információforrás, hanem aktív közösséget is épít. A játékosok megoszthatják tapasztalataikat, véleményeiket és tippjeiket, így segítve egymást a jobb döntések meghozatalában.

Felhasználói Értékelések

A Legjobbkaszino lehetőséget biztosít a játékosoknak, hogy saját tapasztalataikat osszák meg. Ezek a valódi felhasználói vélemények értékes kiegészítést jelentenek a szakértői értékelések mellett.

Rendszeres Frissítések és Hírek

A Legjobbkaszino folyamatosan frissül új tartalmakkal, hírekkel és kaszinóértékelésekkel. A platform követi az iparági változásokat és azonnal tájékoztatja a játékosokat minden fontos fejleményről.

Heti Összefoglalók

Minden héten a Legjobbkaszino összefoglalja a legfontosabb eseményeket, új kaszinókat és promóciókat. Ezek a heti összefoglalók segítenek a játékosoknak naprakésznek maradni anélkül, hogy folyamatosan követniük kellene az összes hírt.

Ügyfélszolgálat és Támogatás

A Legjobbkaszino saját ügyfélszolgálatot is működtet, amely segít a játékosoknak, ha kérdéseik vannak a platform használatával vagy a kaszinóválasztással kapcsolatban. Ez a támogatás növeli a portál értékét és hozzájárul a bizalom építéséhez.

Elérhetőségi Lehetőségek

A Legjobbkaszino több csatornán keresztül is elérhető, beleértve az email-t, az élő csevegést és a közösségi médiát. A csapat gyorsan válaszol a megkeresésekre és segít megoldani a felmerülő problémákat.

Következtetés

A Legjobbkaszino valóban megbízható és átfogó információs forrás minden magyar online kaszinó játékos számára. A platform professzionális értékelései, részletes útmutatói, tematikus toplistái és szakértői tanácsai együttesen teszik a Legjobbkaszinot a magyar szerencsejáték-piac legfontosabb referencia pontjává. A portál folyamatosan fejlődik és bővül, mindig a játékosok érdekeit szem előtt tartva. Akár kezdő, akár tapasztalt játékos vagy, a Legjobbkaszino segít eligazodni a kaszinók világában, megtalálni a legmegfelelőbb platformot és biztonságosan, felelősségteljesen élvezni a szerencsejátékot. Érdemes rendszeresen látogatni a Legjobbkaszino oldalt, hogy mindig naprakész információkkal rendelkezzünk és ne maradjunk le a legjobb ajánlatokról. Ne feledjük azonban, hogy a felelős szerencsejáték kulcsfontosságú - csak olyan összeggel játsszunk, amelynek elvesztését megengedhetjük magunknak, és használjuk ki a Legjobbkaszino által bemutatott önkorlátozási eszközöket a biztonságos játékélményhez. A Legjobbkaszino partnere minden játékosnak az online szerencsejáték izgalmas világában.

NaoBet Kaszinó - Modern Platform Magyar Játékosoknak

A NaoBet kaszinó egy dinamikusan fejlődő platform, amely a modern technológia és a felhasználóbarát dizájn tökéletes ötvözetét kínálja a magyar játékosok számára. Ez az online kaszinó gyorsan népszerűvé vált köszönhetően a gazdag játékválasztéknak, a vonzó bónuszoknak és a kiemelkedően gyors kifizetési rendszerének. A NaoBet különös figyelmet fordít arra, hogy minden játékos kényelmes és biztonságos környezetben élvezhesse a szerencsejátékok világát.

A NaoBet Kaszinó Bemutatása

A NaoBet kaszinó viszonylag friss szereplő a piacon, azonban rövid idő alatt komoly hírnevet szerzett magának. A platform célja, hogy a hagyományos kaszinók erényeit egyesítse a legmodernebb online technológiákkal, így teremtve meg az ideális játékélményt. A kaszinó rendelkezik minden szükséges engedéllyel és licenccel, amelyek garantálják a biztonságos működést.

Alapvető Jellemzők

A NaoBet több területen is megpróbál kiemelkedni a versenytársak közül. Az egyik legfontosabb célkitűzés a gyors és megbízható pénzügyi tranzakciók biztosítása, de ugyanilyen hangsúlyt kap a játékélmény minősége és a felhasználói felület egyszerűsége is.

  • Licenc és szabályozás: A kaszinó rendelkezik érvényes szerencsejáték-engedéllyel, amely biztosítja a tisztességes játékmenetet.
  • Magyar nyelvű felület: A teljes weboldal és ügyfélszolgálat magyar nyelven is elérhető.
  • Mobilbarát dizájn: A platform tökéletesen működik okostelefonokon és tableteken is.
  • Biztonságos tranzakciók: Modern titkosítási technológiák védik a játékosok adatait és pénzügyi információit.
  • Felelős szerencsejáték: Számos eszköz áll rendelkezésre az önkorlátozáshoz és a tudatos játékhoz.

Játékválaszték és Szoftverszolgáltatók

A NaoBet kaszinó játékportfóliója lenyűgöző választékot kínál, amely több ezer címet tartalmaz a legnevesebb szoftverfejlesztőktől. A platform folyamatosan bővíti kínálatát új játékokkal, így a játékosok mindig találnak valami újat és izgalmasat.

Nyerőgépek

A nyerőgépek képezik a NaoBet portfóliójának gerincét, több mint 2000 különböző címmel. Megtalálhatók itt a klasszikus gyümölcsös gépektől kezdve a modern videó nyerőgépeken át egészen a progresszív jackpot játékokig. A legnépszerűbb címek között szerepelnek a Book of Dead, Starburst, Gonzo's Quest és a Mega Moolah.

JátéktípusDarabszámNépszerű Címek
Klasszikus nyerőgépek300+Hot Fruits, Triple Chance, Burning Hot
Videó nyerőgépek1500+Book of Ra, Gonzo's Quest, Dead or Alive
Jackpot játékok50+Mega Moolah, Divine Fortune, Hall of Gods
Megaways200+Bonanza, Extra Chilli, White Rabbit

Asztali Játékok és Élő Kaszinó

Az asztali játékok kedvelői sem maradnak le semmiről a NaoBet kaszinóban. A platform kínálatában megtalálható a rulett, blackjack, baccarat és póker számos változata. Az élő kaszinó szekció pedig valódi krupiérekkel és HD minőségű közvetítéssel teszi még izgalmasabbá a játékélményt.

  • Rulett változatok: Európai, francia, amerikai és különleges változatok, mint a Lightning Roulette.
  • Blackjack asztalok: Klasszikus, VIP és gyors játék opciók különböző téttáblákkal.
  • Baccarat: Hagyományos és modern változatok kezdő és haladó játékosoknak.
  • Póker: Caribbean Stud, Three Card Poker és további izgalmas változatok.

Bónuszok és Promóciók

A NaoBet kaszinó különösen vonzó bónuszrendszert működtet, amely mind az új, mind a visszatérő játékosokat jutalma zza. Az üdvözlő csomag versenyképes a piacon, és gyakran tartalmaz ingyenes pörgetéseket is a befizetési bónusz mellett.

Üdvözlő Csomag

Az új játékosok számára a NaoBet többlépcsős üdvözlő csomagot kínál, amely akár 150 000 forint értékű bónuszt is tartalmazhat az első három befizetés után. Az ingyenes pörgetések népszerű nyerőgépeken használhatók fel, és a bónusz felhasználási feltételei átláthatóak és reálisak.

Folyamatos Promóciók

A rendszeres játékosok számára a NaoBet hetente változó promóciókat biztosít. Ezek között szerepelnek újratöltési bónuszok, cashback ajánlatok, versenyp és tornák, valamint hűségprogramok. A VIP játékosok exkluzív jutalmakban és személyre szabott ajánlatokban részesülnek.

Fizetési Módok és Kifizetések

Az egyik legnagyobb erőssége a NaoBet kaszinónak a gyors és biztonságos pénzügyi tranzakciók rendszere. A platform számos fizetési módot támogat, amely megkönnyíti a magyar játékosok számára a befizetéseket és kifizetéseket forintban.

Befizetési Lehetőségek

A játékosok többféle módon tölthetik fel egyenlegüket a NaoBet kaszinóban. A bankkártyás fizetés mellett elérhetők az elektronikus tárcák és a banki átutalások is. A minimum befizetés általában 1000 forint, ami kezdő játékosok számára is elérhető.

Fizetési MódMinimumMaximumFeldolgozás
Visa/Mastercard1000 Ft500 000 FtAzonnali
Skrill1000 Ft1 000 000 FtAzonnali
Neteller1000 Ft1 000 000 FtAzonnali
Banki átutalás5000 Ft5 000 000 Ft1-2 munkanap

Gyors Kifizetési Folyamat

A NaoBet kaszinó kiemelt figyelmet fordít a gyors kifizetésekre. Az elektronikus pénztárcák esetében a kifizetések gyakran 24 órán belül feldolgozásra kerülnek, míg a bankkártyás kifizetések 1-3 napot vesznek igénybe. A kaszinó törekszi k arra, hogy minden kifizetési kérelmet a lehető leggyorsabban feldolgozzon.

Ügyfélszolgálat és Támogatás

A NaoBet kaszinó professzionális ügyfélszolgálatot működtet, amely magyar nyelven is elérhető. A támogatási csapat segítőkész és gyorsan válaszol minden felmerülő kérdésre vagy problémára.

Elérhetőségi Lehetőségek

A játékosok többféle csatornán keresztül is kapcsolatba léphetnek az ügyfélszolgálattal. Az élő csevegés a leggyorsabb módja a segítségkérésnek, általában néhány percen belül választ kapunk. Az e-mail támogatás részletesebb kérdések esetén hasznos, míg a telefonos ügyfélszolgálat sürgős esetekben elérhető.

  • Élő csevegés: 24/7 rendelkezésre áll, átlagos válaszidő 2-3 perc.
  • E-mail támogatás: Válasz 24 órán belül, részletes információkkal.
  • Telefonos ügyfélszolgálat: Magyar nyelvű támogatás napi 12 órában.
  • GYIK szekció: Átfogó útmutató a leggyakoribb kérdésekhez.

Mobilélmény

A NaoBet kaszinó teljesen mobiloptimalizált, így a játékosok okostelefonon vagy tableten is teljes értékű élményben részesülhetnek. A mobil verzió minden funkciót tartalmaz, ami az asztali változatban elérhető, és a reszponzív dizájnnak köszönhetően automatikusan alkalmazkodik a képernyő méretéhez.

Mobil Funkciók

A mobil platformon keresztül is könnyedén regisztrálhatunk, befizetést végezhetünk, játszhatunk és kivonhatjuk nyereményeinket. A játékok érintésvezérlésre optimalizáltak, így kényelmes és intuitív a használatuk. Nincs szükség külön alkalmazás letöltésére, a böngészőből közvetlenül elérhető minden funkció.

Biztonság és Felelős Játék

A NaoBet kaszinó komolyan veszi a játékosok védelmét és a felelős szerencsejátékot. A platform modern biztonsági technológiákat alkalmaz az adatok és a pénzügyi tranzakciók védelme érdekében. Az SSL titkosítás garantálja, hogy minden érzékeny információ biztonságban marad.

Felelős Szerencsejáték Eszközök

A kaszinó különböző eszközöket biztosít, amelyek segítenek a játékosoknak kontrolláln i játékszokásaikat. Beállíthatunk befizetési limiteket, időkorlátokat és önkizárást is kérhetünk, ha szüksé gesnek érezzük. A NaoBet együttműködik felelős szerencsejáték szervezetekkel, és támogatást nyújt azoknak, akiknek problémát jelent a szerencsejáték.

Összegzés

A NaoBet kaszinó egy modern és megbízható választás a magyar játékosok számára, amely kiváló játékélményt, gyors kifizetéseket és professzionális ügyfélszolgálatot kínál. A gazdag játékválaszték, a vonzó bónuszok és a felhasználóbarát felület együttesen teszik a NaoBetet egy vonzó opcióvá mind a kezdő, mind a tapasztalt játékosok számára. A platform elkötelezettsége a biztonság és a felelős szerencsejáték iránt további garanciát nyújt arra, hogy itt nyugodtan élvezhetjük a kaszinó nyújtotta szórakozást. Minden játékosnak javasoljuk, hogy csak olyan összeggel játsszon, amelynek elvesztését megengedheti magának, és használja ki a platform által kínált felelős játék eszközöket.

New Report

Close