!function(h){"use strict";var i=function(i,o,r){var u,s,v,c,t={},m=0,a=0,l={sensitivity:7,interval:100,timeout:0};function f(e){u=e.clientX,s=e.clientY}function e(e){return a&&(a=clearTimeout(a)),i.removeEventListener("mousemove",f),1!==m&&(v=e.clientX,c=e.clientY,i.addEventListener("mousemove",f),a=setTimeout(function(){!function e(t,n){if(a&&(a=clearTimeout(a)),Math.abs(v-u)+Math.abs(c-s) operation && typeof operation === 'object'); } if (options.operation && typeof options.operation === 'object') { return [ options.operation ]; } return []; } /** * Capture one editor-relative selection snapshot before a batched mutation. * * @param {Object|null} editorHost Active schema editor host. * @param {Object} options Executor options. * @returns {Object|null} Saved cursor/selection snapshot. */ function captureSelectionSnapshot(editorHost, options = {}) { if ( options.restoreCursor === false || !editorHost || typeof editorHost.isSelectionInEditor !== 'function' || typeof editorHost.saveCursorPosition !== 'function' || !editorHost.isSelectionInEditor() ) { return null; } return editorHost.saveCursorPosition(); } /** * Resolve the shared list tracker module. * * @returns {Object|null} List tracker module when loaded. */ function getListTrackerModule() { return SFE.ListBlockTracker || null; } /** * Resolve one live tracker for an editor-bound list root. * * @param {Object|null} editorHost Active schema editor host. * @returns {Object|null} Active list tracker. */ function getTrackerForEditor(editorHost) { const listTracker = getListTrackerModule(); if (!listTracker || !editorHost || !isListElement(editorHost.element)) { return null; } if (editorHost.element._mwpListTracker?.listElement) { return editorHost.element._mwpListTracker; } if (listTracker.active?.listElement === editorHost.element) { return listTracker.active; } if (typeof listTracker.init === 'function') { return listTracker.init(editorHost.element, editorHost.originalBlock || {}); } return null; } /** * Sync one editor instance to the tracker's current root list element. * * Root list type changes replace the editable element. All editor-facing * callers should update their element reference through this helper instead * of assuming the original root still exists. * * @param {Object|null} editorHost Active schema editor host. * @param {Object|null} tracker Active list tracker. * @returns {void} */ function syncEditorRoot(editorHost, tracker) { if (!editorHost || !tracker?.listElement) { return; } if (editorHost.element !== tracker.listElement) { editorHost.element = tracker.listElement; } if (typeof editorHost.syncPlaceholderState === 'function') { editorHost.syncPlaceholderState(); } } /** * Restore one saved editor-relative selection after a batched mutation. * * @param {Object|null} editorHost Active schema editor host. * @param {Object|null} savedSelection Saved cursor/selection snapshot. * @returns {void} */ function restoreSelectionSnapshot(editorHost, savedSelection) { if ( !savedSelection || !editorHost || typeof editorHost.restoreCursorPosition !== 'function' ) { return; } try { if (typeof editorHost.element?.focus === 'function') { editorHost.element.focus(); } } catch (error) { // Focus failures are non-fatal; selection restore below may still work. } editorHost.restoreCursorPosition(savedSelection); } /** * Resolve the current list element targeted by one list-editor selection. * * When the selection is inside a nested list item, the target is that * list item's immediate parent list. If the selection cannot be mapped to * a concrete list item, fall back to the editor root list itself. * * @param {Object|null} editorHost Active schema editor host. * @returns {HTMLElement|null} Target list element. */ function getCurrentListElement(editorHost) { if (!editorHost || !isListElement(editorHost.element)) { return null; } const currentListItem = typeof editorHost.getCurrentListItem === 'function' ? editorHost.getCurrentListItem() : null; if (currentListItem?.parentElement && isListElement(currentListItem.parentElement)) { return currentListItem.parentElement; } const currentList = typeof editorHost.getParentList === 'function' ? editorHost.getParentList() : null; return isListElement(currentList) ? currentList : editorHost.element; } /** * Resolve the schema/public-api list path for one concrete list element. * * @param {Object|null} tracker Active list tracker. * @param {HTMLElement|null} listElement Concrete list element. * @returns {string} Root-relative list path. */ function getListPathForElement(tracker, listElement) { const listTracker = getListTrackerModule(); if (!listTracker || !tracker?.listElement || !isListElement(listElement)) { return ''; } if (typeof listTracker.getPathForList === 'function') { return String(listTracker.getPathForList(tracker, listElement) || ''); } return listElement === tracker.listElement ? '' : ''; } /** * Resolve the schema/public-api item path for one concrete list item. * * @param {Object|null} tracker Active list tracker. * @param {HTMLLIElement|null} listItem Concrete list item. * @returns {string} Root-relative item path. */ function getListItemPathForElement(tracker, listItem) { const listTracker = getListTrackerModule(); if ( !listTracker || !tracker?.listElement || !listItem || listItem.nodeType !== Node.ELEMENT_NODE || listItem.tagName !== 'LI' || typeof listTracker.getPathIndexesForItem !== 'function' ) { return ''; } const pathIndexes = listTracker.getPathIndexesForItem(tracker, listItem); return Array.isArray(pathIndexes) && pathIndexes.length ? pathIndexes.join('_') : ''; } /** * Resolve the current runtime UUID for one concrete list element. * * @param {Object|null} tracker Active list tracker. * @param {HTMLElement|null} listElement Concrete list element. * @returns {string} Session-scoped list UUID. */ function getListRuntimeUuidForElement(tracker, listElement) { const listTracker = getListTrackerModule(); if ( !listTracker || !tracker?.listElement || !isListElement(listElement) || typeof listTracker.getRuntimeUuidForList !== 'function' ) { return ''; } return String(listTracker.getRuntimeUuidForList(tracker, listElement) || ''); } /** * Resolve the current runtime UUID for one concrete list item. * * @param {Object|null} tracker Active list tracker. * @param {HTMLLIElement|null} listItem Concrete list item. * @returns {string} Session-scoped item UUID. */ function getListItemRuntimeUuidForElement(tracker, listItem) { const listTracker = getListTrackerModule(); if ( !listTracker || !tracker?.listElement || !listItem || listItem.nodeType !== Node.ELEMENT_NODE || listItem.tagName !== 'LI' || typeof listTracker.getRuntimeUuidForItem !== 'function' ) { return ''; } return String(listTracker.getRuntimeUuidForItem(tracker, listItem) || ''); } /** * Return the schema-declared editor operations for one active editor. * * @param {Object|null} editorHost Active schema editor host. * @returns {Object[]} Supported schema operation descriptors. */ function getEditorOperations(editorHost) { return Array.isArray(editorHost?.options?.operations) ? editorHost.options.operations.filter(operation => operation && typeof operation === 'object') : []; } /** * Return one component map for the current active editor session. * * @param {Object|null} editorState Active editor state. * @returns {Object} Component metadata keyed by component ID. */ function getActiveEditorComponentMap(editorState) { if (!editorState || !Array.isArray(editorState.editableComponents)) { return {}; } return editorState.editableComponents.reduce((map, component) => { const componentId = String(component?.id || '').trim(); if (!componentId || !component?.element) { return map; } map[componentId] = component; return map; }, {}); } /** * Normalize one candidate string array. * * @param {*} rawValues Candidate values. * @returns {string[]} Trimmed non-empty strings. */ function normalizeStringArray(rawValues) { return Array.isArray(rawValues) ? rawValues .map(value => String(value || '').trim()) .filter(Boolean) : []; } /** * Return one normalized link capability object for the supplied format token. * * @param {Object} inlineCapabilities Available inline format capabilities. * @param {string} formatToken Inline format token. * @returns {Object} Normalized capability data. */ function getInlineLinkCapability(inlineCapabilities, formatToken) { const capability = inlineCapabilities?.[formatToken] && typeof inlineCapabilities[formatToken] === 'object' ? inlineCapabilities[formatToken] : {}; return { ...capability, tag: String(capability.tag || '').trim().toLowerCase(), allowedTargets: normalizeStringArray(capability.allowedTargets), allowedRelTokens: normalizeStringArray(capability.allowedRelTokens).map(value => value.toLowerCase()), allowedProtocols: normalizeStringArray(capability.allowedProtocols).map(value => value.toLowerCase()), autoProtocol: typeof capability.autoProtocol === 'string' ? capability.autoProtocol.trim().toLowerCase() : '', allowsRelativeUrls: capability.allowsRelativeUrls !== false, allowsAnchorLinks: capability.allowsAnchorLinks !== false, preservesUnknownRelTokens: capability.preservesUnknownRelTokens === true, }; } /** * Return whether one candidate attribute payload contains link-like data. * * @param {Object|null} candidateAttributes Candidate link payload. * @returns {boolean} True when link-like keys are present. */ function componentRunAttributesContainLinkData(candidateAttributes) { if (!candidateAttributes || typeof candidateAttributes !== 'object') { return false; } if ( Object.prototype.hasOwnProperty.call(candidateAttributes, 'href') || Object.prototype.hasOwnProperty.call(candidateAttributes, 'url') || Object.prototype.hasOwnProperty.call(candidateAttributes, 'target') || Object.prototype.hasOwnProperty.call(candidateAttributes, 'rel') ) { return true; } return Boolean(candidateAttributes.settings && typeof candidateAttributes.settings === 'object'); } /** * Resolve one raw per-format link attribute payload. * * @param {Object|null} rawFormatAttributes Raw format attribute map. * @param {string} formatToken Current link format token. * @param {string[]} linkFormatTokens Active link format tokens. * @returns {Object} Raw link attribute payload. */ function getComponentRunLinkFormatAttributes(rawFormatAttributes, formatToken, linkFormatTokens) { const source = rawFormatAttributes && typeof rawFormatAttributes === 'object' ? rawFormatAttributes : {}; const directAttributes = source[formatToken]; if (directAttributes && typeof directAttributes === 'object' && !Array.isArray(directAttributes)) { return directAttributes; } if (!Array.isArray(linkFormatTokens) || linkFormatTokens.length !== 1) { return {}; } const misplacedToken = Object.keys(source).find(token => { if (String(token || '').trim() === formatToken) { return false; } return componentRunAttributesContainLinkData(source[token]); }); return misplacedToken ? source[misplacedToken] : {}; } /** * Return whether the supplied rel token is allowed for one link capability. * * @param {string} relToken Candidate rel token. * @param {Object} capability Normalized link capability data. * @returns {boolean} True when the token is allowed. */ function linkCapabilityAllowsRelToken(relToken, capability) { const token = String(relToken || '').trim().toLowerCase(); if (!token) { return false; } return !capability.allowedRelTokens.length || capability.allowedRelTokens.includes(token) || capability.preservesUnknownRelTokens; } /** * Apply one model-facing link settings payload onto raw link attributes. * * @param {Object|null} linkAttributes Current sanitized attributes. * @param {Object|null} settings Model-facing settings payload. * @param {Object} capability Normalized link capability data. * @returns {Object} Merged link attributes. */ function mergeComponentLinkSettingsIntoAttributes(linkAttributes, settings, capability) { const merged = linkAttributes && typeof linkAttributes === 'object' ? { ...linkAttributes } : {}; const source = settings && typeof settings === 'object' ? settings : {}; if (Object.prototype.hasOwnProperty.call(source, 'new_tab')) { if (source.new_tab && capability.allowedTargets.includes('_blank')) { merged.target = '_blank'; } else if (!source.new_tab && merged.target === '_blank') { delete merged.target; } } if (Object.prototype.hasOwnProperty.call(source, 'no_follow')) { const relTokens = Array.from(new Set( String(merged.rel || '') .split(/\s+/) .map(value => String(value || '').trim().toLowerCase()) .filter(Boolean) )); if (source.no_follow && linkCapabilityAllowsRelToken('nofollow', capability)) { relTokens.push('nofollow'); } else if (!source.no_follow) { const nextTokens = relTokens.filter(token => token !== 'nofollow'); relTokens.length = 0; relTokens.push(...nextTokens); } if (relTokens.length) { merged.rel = Array.from(new Set(relTokens)).join(' '); } else { delete merged.rel; } } return merged; } /** * Sanitize one candidate link href against schema capability metadata. * * @param {*} rawHref Candidate raw href or url value. * @param {Object} capability Normalized link capability data. * @returns {string} Sanitized href or an empty string. */ function sanitizeComponentLinkHref(rawHref, capability) { const value = String(rawHref || '').trim(); if (!value) { return ''; } if (value.startsWith('#')) { return capability.allowsAnchorLinks ? value : ''; } if (/^(\/|\.\/|\.\.\/)/.test(value)) { return capability.allowsRelativeUrls ? value : ''; } const protocolMatch = value.match(/^([a-z][a-z0-9+.-]*):/i); if (protocolMatch) { const protocol = String(protocolMatch[1] || '').trim().toLowerCase(); return !capability.allowedProtocols.length || capability.allowedProtocols.includes(protocol) ? value : ''; } if (capability.autoProtocol) { return `${capability.autoProtocol}://${value}`; } return capability.allowsRelativeUrls ? value : ''; } /** * Normalize one raw run format-attribute map into canonical FrontEdit shape. * * @param {Object|null} rawFormatAttributes Raw per-format attribute map. * @param {string[]} formats Active sanitized format tokens. * @param {Object} inlineCapabilities Available inline format capabilities. * @returns {Object} Canonical format-attribute map. */ function normalizeComponentRunFormatAttributes(rawFormatAttributes, formats, inlineCapabilities) { const source = rawFormatAttributes && typeof rawFormatAttributes === 'object' ? rawFormatAttributes : {}; const normalized = {}; const linkFormatTokens = Array.from(new Set( (Array.isArray(formats) ? formats : []) .map(value => String(value || '').trim()) .filter(token => getInlineLinkCapability(inlineCapabilities, token).tag === 'a') )); formats.forEach(formatToken => { const capability = getInlineLinkCapability(inlineCapabilities, formatToken); if (capability.tag !== 'a') { return; } const rawLinkAttributes = getComponentRunLinkFormatAttributes(source, formatToken, linkFormatTokens); if (!rawLinkAttributes || typeof rawLinkAttributes !== 'object') { return; } let linkAttributes = {}; const href = sanitizeComponentLinkHref(rawLinkAttributes.href || rawLinkAttributes.url || '', capability); if (href) { linkAttributes.href = href; } const target = String(rawLinkAttributes.target || '').trim(); if (!capability.allowedTargets.length || capability.allowedTargets.includes(target)) { if (target) { linkAttributes.target = target; } } if (rawLinkAttributes.settings && typeof rawLinkAttributes.settings === 'object') { linkAttributes = mergeComponentLinkSettingsIntoAttributes( linkAttributes, rawLinkAttributes.settings, capability ); } const rel = String(rawLinkAttributes.rel || '').trim(); if (rel) { const normalizedRel = Array.from(new Set( rel .split(/\s+/) .map(value => String(value || '').trim().toLowerCase()) .filter(token => token && linkCapabilityAllowsRelToken(token, capability)) )); if (normalizedRel.length) { linkAttributes.rel = normalizedRel.join(' '); } } if (Object.keys(linkAttributes).length) { normalized[formatToken] = linkAttributes; } }); return normalized; } /** * Normalize one raw structured run list into canonical FrontEdit component runs. * * @param {*} rawRuns Candidate raw run list. * @param {Object} inlineCapabilities Available inline format capabilities. * @returns {Array} Canonical structured runs. */ function normalizeComponentRuns(rawRuns, inlineCapabilities) { if (!Array.isArray(rawRuns)) { return []; } return rawRuns.reduce((runs, run) => { if (!run || typeof run !== 'object' || !Object.prototype.hasOwnProperty.call(run, 'text')) { return runs; } const text = String(run.text || ''); const formats = Array.isArray(run.formats) ? Array.from(new Set( run.formats .map(value => String(value || '').trim()) .filter(token => token && Object.prototype.hasOwnProperty.call(inlineCapabilities || {}, token)) )) : []; const rawFormatAttributes = run.formatAttributes && typeof run.formatAttributes === 'object' ? run.formatAttributes : (run.format_attributes && typeof run.format_attributes === 'object' ? run.format_attributes : {}); runs.push({ text, formats, formatAttributes: normalizeComponentRunFormatAttributes(rawFormatAttributes, formats, inlineCapabilities), }); return runs; }, []); } /** * Read one normalized host-link attribute snapshot from a component element. * * @param {Element|null} element Current host element. * @returns {Object} Existing host link attributes. */ function getExistingComponentHostLinkAttributes(element) { if (!element || element.nodeType !== Node.ELEMENT_NODE) { return {}; } const href = String(element.getAttribute('href') || '').trim(); const target = String(element.getAttribute('target') || '').trim(); const rel = String(element.getAttribute('rel') || '').trim(); const attributes = {}; if (href) { attributes.href = href; } if (target) { attributes.target = target; } if (rel) { attributes.rel = rel; } return attributes; } /** * Build one canonical host-link attribute payload from an operation input. * * @param {Object} operationInput Raw operation payload. * @param {Object} capability Normalized link capability data. * @param {Object} operation Resolved schema operation metadata. * @param {Element} element Host component element. * @returns {Object|null} Canonical link attributes or null when invalid. */ function normalizeComponentHostLinkOperationPayload(operationInput, capability, operation, element) { const payload = operationInput.link && typeof operationInput.link === 'object' ? operationInput.link : (operationInput.attributes && typeof operationInput.attributes === 'object' && !Array.isArray(operationInput.attributes) ? operationInput.attributes : (operationInput.value && typeof operationInput.value === 'object' && !Array.isArray(operationInput.value) ? operationInput.value : operationInput)); let nextAttributes = operation?.preserveUnspecifiedAttributes === false ? {} : getExistingComponentHostLinkAttributes(element); let didReceivePayload = false; if (Object.prototype.hasOwnProperty.call(payload, 'href') || Object.prototype.hasOwnProperty.call(payload, 'url')) { didReceivePayload = true; const href = sanitizeComponentLinkHref(payload.href || payload.url || '', capability); if (href) { nextAttributes.href = href; } else { delete nextAttributes.href; } } if (Object.prototype.hasOwnProperty.call(payload, 'target') || Object.prototype.hasOwnProperty.call(payload, 'linkTarget')) { didReceivePayload = true; const target = String(payload.target || payload.linkTarget || '').trim(); if (!target) { delete nextAttributes.target; } else if (!capability.allowedTargets.length || capability.allowedTargets.includes(target)) { nextAttributes.target = target; } } if (Object.prototype.hasOwnProperty.call(payload, 'rel')) { didReceivePayload = true; const rel = String(payload.rel || '').trim(); if (!rel) { delete nextAttributes.rel; } else { const normalizedRel = Array.from(new Set( rel .split(/\s+/) .map(value => String(value || '').trim().toLowerCase()) .filter(token => token && linkCapabilityAllowsRelToken(token, capability)) )); if (normalizedRel.length) { nextAttributes.rel = normalizedRel.join(' '); } else { delete nextAttributes.rel; } } } const settings = payload.settings && typeof payload.settings === 'object' ? { ...payload.settings } : {}; if (Object.prototype.hasOwnProperty.call(payload, 'new_tab')) { settings.new_tab = payload.new_tab; didReceivePayload = true; } if (Object.prototype.hasOwnProperty.call(payload, 'no_follow')) { settings.no_follow = payload.no_follow; didReceivePayload = true; } if (Object.keys(settings).length) { didReceivePayload = true; nextAttributes = mergeComponentLinkSettingsIntoAttributes(nextAttributes, settings, capability); } return didReceivePayload ? nextAttributes : null; } /** * Apply one normalized host-link attribute payload to a component element. * * @param {Element|null} element Host component element. * @param {Object} linkAttributes Canonical link attributes. * @returns {boolean} True when the mutation applied. */ function applyComponentHostLinkAttributes(element, linkAttributes) { if (!element || element.nodeType !== Node.ELEMENT_NODE || !linkAttributes || typeof linkAttributes !== 'object') { return false; } const href = String(linkAttributes.href || '').trim(); const target = String(linkAttributes.target || '').trim(); const rel = String(linkAttributes.rel || '').trim(); if (href) { element.setAttribute('href', href); if ('href' in element) { element.href = href; } } else { element.removeAttribute('href'); } if (target) { element.setAttribute('target', target); } else { element.removeAttribute('target'); } if (rel) { element.setAttribute('rel', rel); } else { element.removeAttribute('rel'); } return true; } /** * Flatten one line-based rich-text payload into canonical runs. * * This remains an undocumented compatibility input so callers such as ABE * can migrate to direct runs later without FrontEdit reimplementing broker logic * outside the shared executor. * * @param {*} rawLines Candidate raw line list. * @returns {Array} Flat run list with literal newline separators. */ function flattenComponentLineRuns(rawLines) { if (!Array.isArray(rawLines)) { return []; } return rawLines.reduce((runs, line, index) => { if (index > 0) { runs.push({ text: '\n', formats: [], formatAttributes: {}, }); } if (Array.isArray(line?.runs)) { runs.push(...line.runs); } return runs; }, []); } /** * Return the lowercase host tag name for one component element. * * @param {Element|null} hostElement Candidate host element. * @returns {string} Lowercase host tag name. */ function getComponentHostTagName(hostElement) { return hostElement && hostElement.tagName ? String(hostElement.tagName || '').trim().toLowerCase() : ''; } /** * Build one inline format wrapper for a component run token. * * @param {Object} inlineCapabilities Available inline format capabilities. * @param {string} formatToken Format token. * @param {Object|null} formatAttributes Optional format attributes. * @returns {HTMLElement|null} Wrapper element. */ function buildComponentFormatWrapper(inlineCapabilities, formatToken, formatAttributes = null) { const tagName = String(inlineCapabilities?.[formatToken]?.tag || '').trim().toLowerCase(); if (!tagName) { return null; } const element = document.createElement(tagName); if (tagName === 'a' && formatAttributes && typeof formatAttributes === 'object') { const href = String(formatAttributes.href || '').trim(); if (href) { element.setAttribute('href', href); } const target = String(formatAttributes.target || '').trim(); if (target) { element.setAttribute('target', target); } const rel = String(formatAttributes.rel || '').trim(); if (rel) { element.setAttribute('rel', rel); } } return element; } /** * Hoist same-tag inline format attributes onto one host component element. * * @param {Array} runs Structured text runs. * @param {Object} inlineCapabilities Available inline format capabilities. * @param {Element|null} hostElement Live host element. * @returns {{runs:Array,hostAttributes:Object}} Hoisted run data. */ function hoistComponentHostLevelFormats(runs, inlineCapabilities, hostElement = null) { const hostTagName = getComponentHostTagName(hostElement); if (!hostTagName) { return { runs: Array.isArray(runs) ? runs : [], hostAttributes: {}, }; } const hostAttributes = {}; const normalizedRuns = (Array.isArray(runs) ? runs : []).map(run => { const formats = Array.isArray(run?.formats) ? run.formats : []; const formatAttributes = run?.formatAttributes && typeof run.formatAttributes === 'object' ? { ...run.formatAttributes } : {}; const nextFormats = []; formats.forEach(formatToken => { const formatTagName = String(inlineCapabilities?.[formatToken]?.tag || '').trim().toLowerCase(); if (!formatTagName || formatTagName !== hostTagName) { nextFormats.push(formatToken); return; } const attributes = formatAttributes?.[formatToken]; if (attributes && typeof attributes === 'object' && !Array.isArray(attributes)) { Object.assign(hostAttributes, attributes); } delete formatAttributes[formatToken]; }); return { ...run, formats: nextFormats, formatAttributes, }; }); return { runs: normalizedRuns, hostAttributes, }; } /** * Apply hoisted inline-format attributes onto one host element. * * @param {Element|null} hostElement Live host element. * @param {Object|null} hostAttributes Hoisted host attributes. * @returns {void} */ function applyComponentHostLevelFormatAttributes(hostElement, hostAttributes = null) { if (!hostElement || typeof hostElement.setAttribute !== 'function' || typeof hostElement.removeAttribute !== 'function') { return; } const attributes = hostAttributes && typeof hostAttributes === 'object' ? hostAttributes : {}; ['href', 'target', 'rel'].forEach(attributeName => { const value = typeof attributes[attributeName] === 'string' ? attributes[attributeName].trim() : ''; if (value) { hostElement.setAttribute(attributeName, value); return; } hostElement.removeAttribute(attributeName); }); } /** * Build deterministic HTML from one normalized component run sequence. * * @param {Array} runs Structured text runs. * @param {Object} inlineCapabilities Available inline format capabilities. * @param {Object} editorOptions Runtime editor options. * @param {Element|null} hostElement Live host element. * @returns {{html:string,hostAttributes:Object}} Rendered HTML and host attrs. */ function buildComponentHtmlFromRuns(runs, inlineCapabilities, editorOptions = {}, hostElement = null) { const container = document.createElement('div'); const extractionOptions = editorOptions?.options && typeof editorOptions.options === 'object' ? editorOptions.options : {}; const hoisted = hoistComponentHostLevelFormats(runs, inlineCapabilities, hostElement); (Array.isArray(hoisted.runs) ? hoisted.runs : []).forEach(run => { const text = String(run?.text || ''); if (!text) { return; } const segments = extractionOptions.newlinesToBR === true || String(editorOptions?.enterMode || '').trim() === 'linebreak' ? text.split('\n') : [ text ]; segments.forEach((segment, index) => { if (index > 0) { container.appendChild(document.createElement('br')); } if (!segment) { return; } let node = document.createTextNode(segment); (Array.isArray(run.formats) ? run.formats : []).forEach(formatToken => { const wrapper = buildComponentFormatWrapper( inlineCapabilities, formatToken, run.formatAttributes && typeof run.formatAttributes === 'object' ? run.formatAttributes[formatToken] || null : null ); if (!wrapper) { return; } wrapper.appendChild(node); node = wrapper; }); container.appendChild(node); }); }); return { html: container.innerHTML, hostAttributes: hoisted.hostAttributes, }; } /** * Build one plain-text replacement string from normalized runs. * * @param {Array} runs Structured text runs. * @returns {string} Joined plain-text content. */ function buildPlainTextFromComponentRuns(runs) { return (Array.isArray(runs) ? runs : []) .map(run => String(run?.text || '')) .join(''); } /** * Resolve one schema component-content operation from executor options. * * @param {Object|null} editorState Active editor state. * @param {Object} options Executor options. * @returns {Object|null} Matching component operation metadata. */ function resolveComponentOperation(editorState, options = {}) { const operationInput = options.operation && typeof options.operation === 'object' ? options.operation : options; const componentId = String( operationInput.componentId || operationInput.component || operationInput.component_id || '' ).trim(); if (!componentId) { return null; } const component = getActiveEditorComponentMap(editorState)[componentId] || null; const editorOperations = Array.isArray(component?.editorOptions?.operations) ? component.editorOptions.operations.filter(operation => operation && typeof operation === 'object') : []; const suppliedKind = String(operationInput.kind || '').trim(); if (suppliedKind === 'replace_component_content' || suppliedKind === 'text_rewrite') { return { ...operationInput, component: componentId, kind: 'replace_component_content', }; } if (suppliedKind === 'link_change' || suppliedKind === 'inline_link_change') { return { ...operationInput, component: componentId, kind: 'link_change', }; } const suppliedOperationId = String( operationInput.id || operationInput.operationId || '' ).trim(); if (!suppliedOperationId) { return null; } const matched = editorOperations.find(operation => ( String(operation?.id || '').trim() === suppliedOperationId && ( String(operation?.kind || '').trim() === 'text_rewrite' || String(operation?.kind || '').trim() === 'replace_component_content' || String(operation?.kind || '').trim() === 'link_change' ) )) || null; if (!matched) { return null; } return { ...matched, component: componentId, kind: String(matched.kind || '').trim() === 'link_change' ? 'link_change' : 'replace_component_content', }; } /** * Execute one or more schema-backed component-content replacements. * * @param {Object} options Executor options. * @returns {Object|null} Applied-operation summary. */ function executeComponentOperations(options = {}) { const editorState = options.editorState || null; const editorHost = options.editorHost || null; const componentMap = getActiveEditorComponentMap(editorState); const inputOperations = Array.isArray(options.operations) ? options.operations : [ options.operation || options ]; if (!editorState || !editorHost || !Object.keys(componentMap).length || !inputOperations.length) { return null; } const savedSelection = captureSelectionSnapshot(editorHost, options); const results = []; const updatedComponentIds = []; inputOperations.forEach(inputOperation => { const operationInput = inputOperation && typeof inputOperation === 'object' && !Array.isArray(inputOperation) ? inputOperation : {}; const operation = resolveComponentOperation(editorState, operationInput); if (!operation) { return; } const componentId = String(operation.component || '').trim(); const component = componentMap[componentId] || null; if (!component || !component.element) { return; } const inlineCapabilities = ( component.editorOptions?.inlineFormatCapabilities && typeof component.editorOptions.inlineFormatCapabilities === 'object' ) ? component.editorOptions.inlineFormatCapabilities : {}; if (operation.kind === 'link_change') { const formatToken = String(operation.format || operationInput.format || '').trim(); const capability = getInlineLinkCapability(inlineCapabilities, formatToken); const hostTagName = String(component.element?.tagName || '').trim().toLowerCase(); if (capability.tag !== 'a' || hostTagName !== capability.tag) { return; } const linkAttributes = normalizeComponentHostLinkOperationPayload( operationInput, capability, operation, component.element ); if (!linkAttributes || !applyComponentHostLinkAttributes(component.element, linkAttributes)) { return; } } else { const bindingSource = String( operationInput.bindingSource || component?.bindingSource || '' ).trim().toLowerCase(); if (bindingSource !== 'html' && bindingSource !== 'plaintext') { return; } const rawRuns = Array.isArray(operationInput.runs) ? operationInput.runs : flattenComponentLineRuns(operationInput.lines); const hasDirectTextPayload = Object.prototype.hasOwnProperty.call(operationInput, 'text'); const runs = normalizeComponentRuns( hasDirectTextPayload && bindingSource === 'plaintext' && !rawRuns.length ? [ { text: String(operationInput.text || ''), formats: [], formatAttributes: {} } ] : rawRuns, inlineCapabilities ); const hasReplacementPayload = hasDirectTextPayload || Array.isArray(operationInput.runs) || Array.isArray(operationInput.lines); if (!hasReplacementPayload) { return; } if (bindingSource === 'plaintext') { component.element.textContent = hasDirectTextPayload && !rawRuns.length ? String(operationInput.text || '') : buildPlainTextFromComponentRuns(runs); } else { const rendered = buildComponentHtmlFromRuns( runs, inlineCapabilities, component.editorOptions || {}, component.element ); component.element.innerHTML = rendered.html; applyComponentHostLevelFormatAttributes(component.element, rendered.hostAttributes); } } updatedComponentIds.push(componentId); results.push({ id: String(operation.id || '').trim(), kind: operation.kind === 'link_change' ? 'link_change' : 'replace_component_content', componentId, applied: true, }); }); if (!results.length) { return null; } restoreSelectionSnapshot(editorHost, savedSelection); if (typeof options.afterSync === 'function') { options.afterSync({ editorState, editorHost, results, updatedComponentIds: Array.from(new Set(updatedComponentIds)), }); } if (typeof editorHost.updateToolbarState === 'function') { editorHost.updateToolbarState(); } const historyApi = getSessionHistoryApiForHost(editorHost); if (options.saveHistory !== false && typeof historyApi?.saveToHistory === 'function') { historyApi.saveToHistory(); } return { results, updatedComponentIds: Array.from(new Set(updatedComponentIds)), operationsApplied: results .map(result => result.id || result.kind) .filter(Boolean), }; } /** * Resolve one schema component-media operation from executor options. * * @param {Object|null} editorState Active editor state. * @param {Object} options Executor options. * @returns {Object|null} Matching component-media operation metadata. */ function resolveMediaOperation(editorState, options = {}) { const operationInput = options.operation && typeof options.operation === 'object' ? options.operation : options; const componentId = String( operationInput.componentId || operationInput.component || operationInput.component_id || '' ).trim(); if (!componentId) { return null; } const component = getActiveEditorComponentMap(editorState)[componentId] || null; if (!component?.element || !component?.mediaDescriptor) { return null; } const suppliedKind = String(operationInput.kind || '').trim(); if (suppliedKind === 'replace_component_media') { return { ...operationInput, component: componentId, kind: 'replace_component_media', }; } return null; } /** * Execute one or more schema-backed component-media replacements through the * active media-session host. * * @param {Object} options Executor options. * @returns {Object|null} Applied-operation summary. */ function executeMediaOperations(options = {}) { const editorState = options.editorState || null; const editorHost = options.editorHost || null; const componentMap = getActiveEditorComponentMap(editorState); const inputOperations = Array.isArray(options.operations) ? options.operations : [ options.operation || options ]; if ( !editorState || !editorHost || typeof editorHost.applyMediaSelection !== 'function' || !Object.keys(componentMap).length || !inputOperations.length ) { return null; } const activeComponentId = String( editorState.activeComponentId || editorState.activeEditableComponent?.id || '' ).trim(); const results = []; const updatedComponentIds = []; inputOperations.forEach(inputOperation => { const operationInput = inputOperation && typeof inputOperation === 'object' && !Array.isArray(inputOperation) ? inputOperation : {}; const operation = resolveMediaOperation(editorState, operationInput); if (!operation) { return; } const componentId = String(operation.component || '').trim(); const component = componentMap[componentId] || null; if (!component?.element || !component?.mediaDescriptor) { return; } if (activeComponentId && activeComponentId !== componentId) { return; } if (!activeComponentId && editorHost.element && editorHost.element !== component.element) { return; } const payload = operationInput.media && typeof operationInput.media === 'object' ? operationInput.media : (operationInput.value && typeof operationInput.value === 'object' ? operationInput.value : operationInput); const url = String(payload?.url || '').trim(); if (!url) { return; } const attachmentId = Object.prototype.hasOwnProperty.call(payload, 'attachmentId') ? payload.attachmentId : (Object.prototype.hasOwnProperty.call(payload, 'attachment_id') ? payload.attachment_id : null); const source = String( payload?.source || payload?.fromState || '' ).trim().toLowerCase() === 'library' ? 'library' : 'input'; const didApply = editorHost.applyMediaSelection(url, attachmentId, { fromState: source, }); if (didApply === false) { return; } updatedComponentIds.push(componentId); results.push({ id: String(operation.id || '').trim(), kind: 'replace_component_media', componentId, applied: true, }); }); if (!results.length) { return null; } if (typeof options.afterSync === 'function') { options.afterSync({ editorState, editorHost, results, updatedComponentIds: Array.from(new Set(updatedComponentIds)), }); } return { results, updatedComponentIds: Array.from(new Set(updatedComponentIds)), operationsApplied: results .map(result => result.id || result.kind) .filter(Boolean), }; } /** * Normalize one primitive list-operation kind to the canonical * tracker/editor operation kind. * * @param {string} kindRaw Candidate operation kind. * @returns {string} Canonical primitive operation kind or an empty string. */ function normalizePrimitiveListOperationKind(kindRaw) { const kind = String(kindRaw || '').trim().toLowerCase(); const supportedKinds = new Set([ 'insert_list_item', 'remove_list_item', 'move_list_item', 'indent_list_item', 'outdent_list_item', 'update_list_item_text', 'toggle_list_type', ]); return supportedKinds.has(kind) ? kind : ''; } /** * Normalize one public list-operation kind to the documented runtime API * vocabulary. * * Public callers intentionally use a higher-level token family than the * primitive tracker/editor operations so the external contract stays explicit * while FrontEdit keeps the low-level DOM mutation layer internal. * * @param {string} kindRaw Candidate operation kind. * @returns {string} Canonical public operation kind or an empty string. */ function normalizePublicApiListOperationKind(kindRaw) { const kind = String(kindRaw || '').trim().toLowerCase(); const supportedKinds = new Set([ 'update_list_item_text', 'insert_before', 'insert_after', 'insert_child', 'remove_list_item', 'move_before', 'move_after', 'indent_list_item', 'outdent_list_item', 'toggle_list_type', ]); return supportedKinds.has(kind) ? kind : ''; } /** * Normalize one list-operation kind for the current execution surface. * * Public API calls use the higher-level UUID-oriented vocabulary, while * internal editor/schema calls keep the primitive tracker operation kinds. * * @param {string} kindRaw Candidate operation kind. * @param {Object} options Executor options. * @returns {string} Canonical operation kind or an empty string. */ function normalizeListOperationKind(kindRaw, options = {}) { return getListTargetResolutionMode(options) === 'api_uuid' ? normalizePublicApiListOperationKind(kindRaw) : normalizePrimitiveListOperationKind(kindRaw); } /** * Return whether one operation payload explicitly defines a given key. * * @param {Object|null} operation Candidate operation payload. * @param {string} key Key to check. * @returns {boolean} True when the key exists on the payload. */ function hasOwnOperationKey(operation, key) { return !!( operation && typeof operation === 'object' && !Array.isArray(operation) && Object.prototype.hasOwnProperty.call(operation, key) ); } /** * Return the caller target-resolution mode for list operations. * * `selection` allows editor-originated calls to omit the item/list target and * resolve it from the current DOM selection. `explicit` requires path/list * tokens. `api_uuid` requires the public runtime UUID token family. * * @param {Object} options Executor options. * @returns {string} `selection`, `explicit`, or `api_uuid`. */ function getListTargetResolutionMode(options = {}) { const mode = String(options.targetResolutionMode || '').trim().toLowerCase(); if (mode === 'explicit') { return 'explicit'; } if (mode === 'api_uuid') { return 'api_uuid'; } return 'selection'; } /** * Return whether one normalized list operation targets a list item. * * @param {string} kind Canonical list operation kind. * @returns {boolean} True when the operation acts on one item path. */ function isItemTargetListOperation(kind) { return ( kind === 'remove_list_item' || kind === 'indent_list_item' || kind === 'outdent_list_item' || kind === 'update_list_item_text' || kind === 'move_list_item' ); } /** * Return whether one normalized list operation uses structural item-path * anchors without using only one direct `path`. * * @param {string} kind Canonical list operation kind. * @returns {boolean} True when the operation uses before/after/parent paths. */ function isPathAnchoredListOperation(kind) { return ( kind === 'insert_list_item' || kind === 'move_list_item' ); } /** * Return whether one normalized list operation moves an existing item. * * @param {string} kind Canonical list operation kind. * @returns {boolean} True when the operation moves one existing item. */ function isMoveListOperation(kind) { return kind === 'move_list_item'; } /** * Return whether one insert/move operation defines any supported path anchor. * * @param {Object} operationInput Raw operation payload. * @returns {boolean} True when one path anchor exists. */ function hasAnyStructuralPathAnchor(operationInput) { return ( hasOwnOperationKey(operationInput, 'path') || hasOwnOperationKey(operationInput, 'itemPath') || hasOwnOperationKey(operationInput, 'item_path') || hasOwnOperationKey(operationInput, 'beforePath') || hasOwnOperationKey(operationInput, 'before_path') || hasOwnOperationKey(operationInput, 'afterPath') || hasOwnOperationKey(operationInput, 'after_path') || hasOwnOperationKey(operationInput, 'parentPath') || hasOwnOperationKey(operationInput, 'parent_path') ); } /** * Return whether one raw operation includes any public UUID list-item target. * * @param {Object} operationInput Raw operation payload. * @returns {boolean} True when one item UUID token exists. */ function hasAnyUuidItemTarget(operationInput) { return ( hasOwnOperationKey(operationInput, 'itemUuid') || hasOwnOperationKey(operationInput, 'newItemUuid') || hasOwnOperationKey(operationInput, 'targetItemUuid') ); } /** * Return whether one public operation payload contains any path fields. * * @param {Object} operationInput Raw operation payload. * @returns {boolean} True when a path token exists. */ function hasAnyPathTarget(operationInput) { return hasAnyStructuralPathAnchor(operationInput) || hasOwnOperationKey(operationInput, 'listPath') || hasOwnOperationKey(operationInput, 'list_path'); } /** * Return whether one public payload contains only the documented keys for one * public list-operation kind. * * Unknown keys are rejected. The public list API is intentionally strict so * callers must match the documented v1 shape exactly. * * @param {Object} operationInput Raw operation payload. * @param {string} normalizedKind Canonical public operation kind. * @returns {boolean} True when no unsupported keys are present. */ function hasOnlySupportedApiUuidKeys(operationInput, normalizedKind) { const allowedKeys = new Set([ 'kind', 'contentHtml', 'contentText', ]); if ( normalizedKind === 'update_list_item_text' || normalizedKind === 'remove_list_item' || normalizedKind === 'indent_list_item' || normalizedKind === 'outdent_list_item' ) { allowedKeys.add('itemUuid'); } else if (normalizedKind === 'insert_before' || normalizedKind === 'insert_after') { allowedKeys.add('newItemUuid'); allowedKeys.add('targetItemUuid'); } else if (normalizedKind === 'insert_child') { allowedKeys.add('newItemUuid'); allowedKeys.add('targetItemUuid'); } else if (normalizedKind === 'move_before' || normalizedKind === 'move_after') { allowedKeys.add('itemUuid'); allowedKeys.add('targetItemUuid'); } else if (normalizedKind === 'toggle_list_type') { allowedKeys.add('itemUuid'); } return Object.keys(operationInput || {}).every(key => allowedKeys.has(key)); } /** * Validate that one explicit public list operation uses only the documented * target token family for its kind. * * @param {Object} operationInput Raw operation payload. * @param {string} normalizedKind Canonical list operation kind. * @returns {boolean} True when the target token shape is valid. */ function isValidExplicitListOperationTarget(operationInput, normalizedKind) { const hasPath = hasOwnOperationKey(operationInput, 'path') || hasOwnOperationKey(operationInput, 'itemPath') || hasOwnOperationKey(operationInput, 'item_path'); const hasListPath = hasOwnOperationKey(operationInput, 'listPath') || hasOwnOperationKey(operationInput, 'list_path'); if (normalizedKind === 'toggle_list_type') { return hasListPath && !hasPath; } if (isItemTargetListOperation(normalizedKind)) { return hasPath && !hasListPath; } if (isMoveListOperation(normalizedKind)) { return hasPath && !hasListPath; } if (isPathAnchoredListOperation(normalizedKind)) { return hasAnyStructuralPathAnchor(operationInput) && !hasListPath; } return true; } /** * Validate that one public API list operation uses only the UUID token family. * * API callers must treat runtime UUIDs as the external cursor model. Paths * remain internal execution details resolved by FrontEdit immediately before each * operation runs. * * @param {Object} operationInput Raw operation payload. * @param {string} normalizedKind Canonical list operation kind. * @returns {boolean} True when the UUID target shape is valid. */ function isValidApiUuidListOperationTarget(operationInput, normalizedKind) { if (!hasOnlySupportedApiUuidKeys(operationInput, normalizedKind)) { return false; } const hasItemUuid = hasOwnOperationKey(operationInput, 'itemUuid'); const hasNewItemUuid = hasOwnOperationKey(operationInput, 'newItemUuid'); const hasListUuid = hasOwnOperationKey(operationInput, 'listUuid'); const hasTargetItemUuid = hasOwnOperationKey(operationInput, 'targetItemUuid'); if (normalizedKind === 'toggle_list_type') { return hasItemUuid && !hasNewItemUuid && !hasListUuid && !hasTargetItemUuid; } if ( normalizedKind === 'update_list_item_text' || normalizedKind === 'remove_list_item' || normalizedKind === 'indent_list_item' || normalizedKind === 'outdent_list_item' ) { return hasItemUuid && !hasNewItemUuid && !hasListUuid && !hasTargetItemUuid; } if (normalizedKind === 'insert_before' || normalizedKind === 'insert_after') { return !hasItemUuid && hasNewItemUuid && !hasListUuid && hasTargetItemUuid; } if (normalizedKind === 'insert_child') { return !hasItemUuid && hasNewItemUuid && !hasListUuid && hasTargetItemUuid; } if (normalizedKind === 'move_before' || normalizedKind === 'move_after') { return hasItemUuid && !hasNewItemUuid && !hasListUuid && hasTargetItemUuid; } return false; } /** * Expand one public API list operation into one or more public operation * inputs that can be resolved against the live tree one step at a time. * * `insert_child` intentionally fans out into `insert_after` and `indent_list_item` so * the second primitive resolution runs after the new list item exists. * * @param {Object} operationInput Raw public operation payload. * @returns {Object[]} Expanded public operation inputs. */ function expandApiUuidListOperationInputs(operationInput) { const normalizedKind = normalizePublicApiListOperationKind(operationInput?.kind); if (!normalizedKind) { return []; } if (normalizedKind !== 'insert_child') { return [ { ...operationInput, kind: normalizedKind, }, ]; } return [ { ...operationInput, kind: 'insert_after', }, { kind: 'indent_list_item', itemUuid: operationInput.newItemUuid, }, ]; } /** * Resolve one public API UUID-targeted list operation to one internal * primitive operation payload with concrete path tokens. * * This runs against the current live mutated tree so later batched operations * can reuse the same stable runtime UUIDs while FrontEdit recalculates fresh paths * before each mutation. * * @param {Object|null} tracker Active list tracker. * @param {Object} operation Normalized public operation payload. * @returns {Object|null} Path-resolved primitive tracker operation payload. */ function resolveApiUuidListOperationTargets(tracker, operation) { const listTracker = getListTrackerModule(); if (!listTracker || !tracker?.listElement || !operation || typeof operation !== 'object') { return null; } const publicKind = normalizePublicApiListOperationKind(operation.kind); if (!publicKind) { return null; } const resolvedOperation = { ...operation, }; const itemUuid = String(resolvedOperation.itemUuid ?? '').trim(); const newItemUuid = String(resolvedOperation.newItemUuid ?? '').trim(); const listUuid = String(resolvedOperation.listUuid ?? '').trim(); const targetItemUuid = String(resolvedOperation.targetItemUuid ?? '').trim(); let targetItem = null; let currentItem = null; if (itemUuid) { currentItem = typeof listTracker.getItemByRuntimeUuid === 'function' ? listTracker.getItemByRuntimeUuid(tracker, itemUuid) : null; const targetPath = getListItemPathForElement(tracker, currentItem); if (!targetPath) { return null; } resolvedOperation.path = targetPath; } if (targetItemUuid) { targetItem = typeof listTracker.getItemByRuntimeUuid === 'function' ? listTracker.getItemByRuntimeUuid(tracker, targetItemUuid) : null; if (!targetItem) { return null; } } if (listUuid) { const targetList = typeof listTracker.getListByRuntimeUuid === 'function' ? listTracker.getListByRuntimeUuid(tracker, listUuid) : null; if (!targetList) { return null; } resolvedOperation.listPath = getListPathForElement(tracker, targetList); } if (newItemUuid) { resolvedOperation.newItemUuid = newItemUuid; resolvedOperation.itemUuid = newItemUuid; } if ( publicKind === 'update_list_item_text' || publicKind === 'remove_list_item' || publicKind === 'indent_list_item' || publicKind === 'outdent_list_item' ) { resolvedOperation.kind = publicKind; } else if (publicKind === 'insert_before' || publicKind === 'insert_after') { const targetPath = getListItemPathForElement(tracker, targetItem); if (!targetPath) { return null; } resolvedOperation.kind = 'insert_list_item'; if (publicKind === 'insert_before') { resolvedOperation.beforePath = targetPath; } else { resolvedOperation.afterPath = targetPath; } } else if (publicKind === 'move_before' || publicKind === 'move_after') { const targetPath = getListItemPathForElement(tracker, targetItem); if (!targetPath) { return null; } resolvedOperation.kind = 'move_list_item'; if (publicKind === 'move_before') { resolvedOperation.beforePath = targetPath; } else { resolvedOperation.afterPath = targetPath; } } else if (publicKind === 'toggle_list_type') { const containingList = currentItem?.parentElement; const containingListPath = getListPathForElement(tracker, containingList); if (!containingListPath && containingList !== tracker.listElement) { return null; } resolvedOperation.listPath = containingListPath; resolvedOperation.kind = 'toggle_list_type'; } else { return null; } delete resolvedOperation.listUuid; delete resolvedOperation.targetItemUuid; return resolvedOperation; } /** * Resolve one schema list-structure operation from executor options. * * @param {Object|null} editorHost Active schema editor host. * @param {Object} options Executor options. * @returns {Object|null} Matching list operation metadata. */ function resolveListOperation(editorHost, options = {}, executorOptions = {}) { if (options.operation && typeof options.operation === 'object') { const suppliedOperation = options.operation; const suppliedKind = normalizeListOperationKind(suppliedOperation.kind, executorOptions); if (suppliedKind) { return { ...suppliedOperation, kind: suppliedKind, }; } const suppliedOperationId = String( suppliedOperation.id || suppliedOperation.operationId || '' ).trim(); if (suppliedOperationId) { return getEditorOperations(editorHost).find(operation => ( String(operation?.id || '').trim() === suppliedOperationId && !!normalizePrimitiveListOperationKind(operation?.kind) )) || null; } } const operationId = String(options.operationId || options.id || '').trim(); if (!operationId) { return null; } return getEditorOperations(editorHost).find(operation => ( String(operation?.id || '').trim() === operationId && !!normalizePrimitiveListOperationKind(operation?.kind) )) || null; } /** * Resolve one schema block-attribute operation from executor options. * * @param {Object|null} editorHost Active schema editor host. * @param {Object} options Executor options. * @returns {Object|null} Matching block-attribute operation. */ function resolveBlockAttributeOperation(editorHost, options = {}) { if (options.operation && typeof options.operation === 'object') { const suppliedOperation = options.operation; if (String(suppliedOperation.kind || '').trim() === 'block_attribute_change') { return suppliedOperation; } const suppliedOperationId = String( suppliedOperation.id || suppliedOperation.operationId || '' ).trim(); if (suppliedOperationId) { return getEditorOperations(editorHost).find(operation => ( String(operation?.id || '').trim() === suppliedOperationId && String(operation?.kind || '').trim() === 'block_attribute_change' )) || null; } } const operationId = String(options.operationId || options.id || '').trim(); if (!operationId) { return null; } return getEditorOperations(editorHost).find(operation => ( String(operation?.id || '').trim() === operationId && String(operation?.kind || '').trim() === 'block_attribute_change' )) || null; } /** * Build one executable structural list operation for the tracker layer. * * Toolbar actions may provide only a schema operation ID, while public-api * callers generally provide canonical payload kinds. This helper resolves * either shape and fills in the active item/list path when the caller is * targeting the current selection. * * @param {Object|null} editorHost Active schema editor host. * @param {Object|null} tracker Active list tracker. * @param {Object} input Raw executor operation input. * @returns {Object|null} Normalized tracker operation payload. */ function buildExecutableListOperation(editorHost, tracker, input = {}, options = {}) { const operationInput = input && typeof input === 'object' && !Array.isArray(input) ? input : {}; const resolvedOperation = resolveListOperation(editorHost, operationInput, options); const baseOperation = resolvedOperation && typeof resolvedOperation === 'object' ? resolvedOperation : operationInput; const normalizedKind = normalizeListOperationKind(baseOperation.kind || operationInput.kind, options); if (!normalizedKind) { return null; } const targetResolutionMode = getListTargetResolutionMode(options); if ( targetResolutionMode === 'explicit' && !isValidExplicitListOperationTarget(operationInput, normalizedKind) ) { return null; } if ( targetResolutionMode === 'api_uuid' && !isValidApiUuidListOperationTarget(operationInput, normalizedKind) ) { return null; } const mergedOperation = { ...baseOperation, ...operationInput, kind: normalizedKind, }; if (targetResolutionMode === 'api_uuid') { delete mergedOperation.listItem; delete mergedOperation.listElement; return resolveApiUuidListOperationTargets(tracker, mergedOperation); } const explicitListItem = operationInput.listItem instanceof Element && operationInput.listItem.tagName === 'LI' ? operationInput.listItem : null; const currentListItem = explicitListItem || ( typeof editorHost?.getCurrentListItem === 'function' ? editorHost.getCurrentListItem() : null ); const itemPath = String( mergedOperation.path ?? mergedOperation.itemPath ?? mergedOperation.item_path ?? '' ).trim(); if ( !itemPath && normalizedKind !== 'insert_list_item' && normalizedKind !== 'move_list_item' && normalizedKind !== 'toggle_list_type' ) { if (targetResolutionMode === 'explicit') { return null; } const currentItemPath = getListItemPathForElement(tracker, currentListItem); if (!currentItemPath) { return null; } mergedOperation.path = currentItemPath; } if (normalizedKind === 'toggle_list_type') { const targetList = operationInput.listElement || getCurrentListElement(editorHost); const listPath = String( mergedOperation.listPath ?? mergedOperation.list_path ?? '' ).trim(); if (!listPath && targetResolutionMode === 'explicit') { return null; } if (!listPath && targetList) { mergedOperation.listPath = getListPathForElement(tracker, targetList); } } delete mergedOperation.listItem; delete mergedOperation.listElement; return mergedOperation; } /** * Return the explicit current value for one tracked block attribute. * * @param {Object|null} editorHost Active schema editor host. * @param {string} attributePath Canonical block attribute path. * @returns {*} Tracked value when present. */ function getTrackedAttributeValue(editorHost, attributePath) { if (!editorHost || typeof editorHost.getTrackedAttributeValue !== 'function') { return undefined; } return editorHost.getTrackedAttributeValue(attributePath); } /** * Return the current heading level represented by the live editor element. * * @param {Object|null} editorHost Active schema editor host. * @returns {number|null} Current heading level when applicable. */ function getCurrentHeadingLevel(editorHost) { const trackedLevel = getTrackedAttributeValue(editorHost, 'level'); const parsedTrackedLevel = Number.parseInt(trackedLevel, 10); if (Number.isInteger(parsedTrackedLevel) && parsedTrackedLevel >= 1 && parsedTrackedLevel <= 6) { return parsedTrackedLevel; } const tagName = String(editorHost?.element?.tagName || '').trim().toLowerCase(); if (/^h[1-6]$/.test(tagName)) { return Number.parseInt(tagName.slice(1), 10); } return null; } /** * Return the current textAlignment represented by the schema-backed editor state. * * @param {Object|null} editorHost Active schema editor host. * @param {Object|null} operation Schema block-attribute operation metadata. * @returns {string|undefined} Current normalized textAlignment value. */ function getCurrentTextAlignmentValue(editorHost, operation = null) { const textAlignmentCapability = getNormalizedTextAlignmentCapability(editorHost, operation); if (!textAlignmentCapability || !isTextAlignmentOperation(editorHost, operation)) { return undefined; } const isVirtualBindingBackedTextAlignment = isVirtualBindingBackedTextAlignmentCapability(textAlignmentCapability); if ( isVirtualBindingBackedTextAlignment && editorHost && typeof editorHost.getTextAlignmentState === 'function' ) { const domTextAlignment = String(editorHost.getTextAlignmentState() || '').trim().toLowerCase(); if (domTextAlignment) { return domTextAlignment; } return textAlignmentCapability.normalizedUnsetValue; } const trackedTextAlignment = getTrackedAttributeValue(editorHost, textAlignmentCapability.attribute); if (typeof trackedTextAlignment === 'string' && trackedTextAlignment.trim()) { return trackedTextAlignment.trim().toLowerCase(); } if (typeof trackedTextAlignment === 'undefined') { if (editorHost && typeof editorHost.getTextAlignmentState === 'function') { const domTextAlignment = String(editorHost.getTextAlignmentState() || '').trim().toLowerCase(); if (domTextAlignment) { return domTextAlignment; } } return textAlignmentCapability.normalizedUnsetValue; } return textAlignmentCapability.normalizedUnsetValue; } /** * Determines whether the given textAlignment capability relies on a virtual data-binding * (explicitly checking if the attribute is strictly named 'textAlignment'). * * @param {Object|null|undefined} textAlignmentCapability - The textAlignment capability configuration object to evaluate. * @returns {boolean} True if the capability is backed by a virtual binding; otherwise false. */ function isVirtualBindingBackedTextAlignmentCapability(textAlignmentCapability) { return !!( textAlignmentCapability && ( textAlignmentCapability.attribute === 'textAlignment' || textAlignmentCapability.attribute === 'columnAlignment' ) ); } /** * Determine whether one resolved block-attribute operation is the active * schema-backed textAlignment operation for this editor. * * @param {Object|null} editorHost Active schema editor host. * @param {Object|null} operation Schema block-attribute operation metadata. * @returns {boolean} True when this operation targets textAlignment. */ function isTextAlignmentOperation(editorHost, operation = null) { if (!operation || typeof operation !== 'object') { return false; } const textAlignmentCapability = getNormalizedTextAlignmentCapability(editorHost, operation); if (!textAlignmentCapability) { return false; } const operationAttribute = typeof operation.attribute === 'string' ? operation.attribute.trim() : ''; return !!operationAttribute && operationAttribute === textAlignmentCapability.attribute; } /** * Return the normalized schema-backed textAlignment capability for one editor. * * Centralizes the canonical textAlignment attribute lookup so both the executor and * editor mutation helpers read the same normalized shape. * * @param {Object|null} editorHost Active schema editor host. * @returns {Object|null} Normalized textAlignment capability or null. */ function getNormalizedTextAlignmentCapability(editorHost, operation = null) { if (!editorHost || typeof editorHost.getAttributeCapability !== 'function') { return null; } const operationAttribute = typeof operation?.attribute === 'string' ? operation.attribute.trim() : ''; const capabilityKey = operationAttribute === 'columnAlignment' ? 'columnAlignment' : 'textAlignment'; const textAlignmentCapability = editorHost.getAttributeCapability(capabilityKey); if (!textAlignmentCapability || typeof textAlignmentCapability !== 'object') { return null; } const attribute = typeof textAlignmentCapability.attribute === 'string' ? textAlignmentCapability.attribute.trim() : ''; if (!attribute) { return null; } const values = Array.isArray(textAlignmentCapability.values) ? textAlignmentCapability.values .map(value => String(value).trim().toLowerCase()) .filter(Boolean) : []; const hasUnsetValue = Object.prototype.hasOwnProperty.call(textAlignmentCapability, 'unsetValue'); const unsetValue = hasUnsetValue ? textAlignmentCapability.unsetValue : undefined; const normalizedUnsetValue = typeof unsetValue === 'string' ? unsetValue.trim().toLowerCase() : unsetValue; return { ...textAlignmentCapability, attribute, values, unsetValue, normalizedUnsetValue, }; } /** * Normalize one candidate block-attribute value for unset-value comparison. * * String-based schema values such as alignment tokens compare case-insensitively, * while numeric/boolean/null values preserve their original scalar semantics. * * @param {*} value Candidate attribute value. * @returns {*} Normalized comparison value. */ function normalizeBlockAttributeComparisonValue(value) { return typeof value === 'string' ? value.trim().toLowerCase() : value; } /** * Determine whether one requested block-attribute value maps to the schema's * declared unset/default value. * * @param {Object|null} descriptor Operation/capability metadata with optional `unsetValue`. * @param {*} value Candidate attribute value. * @returns {boolean} True when the value represents the unset state. */ function isUnsetBlockAttributeValue(descriptor, value) { if (!descriptor || !Object.prototype.hasOwnProperty.call(descriptor, 'unsetValue')) { return false; } return normalizeBlockAttributeComparisonValue(value) === normalizeBlockAttributeComparisonValue(descriptor.unsetValue); } /** * Normalize one requested block-attribute value for tracked attr storage. * * Schema-declared unset values are converted to `undefined` so the canonical * save path removes the corresponding attribute instead of persisting the * default marker itself. * * @param {Object|null} descriptor Operation/capability metadata with optional `unsetValue`. * @param {*} value Requested attribute value. * @returns {*} Storage-ready tracked value. */ function normalizeBlockAttributeTrackedValue(descriptor, value) { return isUnsetBlockAttributeValue(descriptor, value) ? undefined : value; } /** * Return the current block-attribute value for one schema operation. * * @param {Object|null} editorHost Active schema editor host. * @param {Object|null} operation Schema block-attribute operation metadata. * @returns {*} Current effective attribute value. */ function getCurrentBlockAttributeValue(editorHost, operation = null) { const attributePath = String(operation?.attribute || '').trim(); if (!attributePath) { return undefined; } if (attributePath === 'level') { return getCurrentHeadingLevel(editorHost); } if (operation?.tagChange === true) { return String(editorHost?.element?.tagName || '').trim().toLowerCase() || undefined; } if (attributePath === 'align' && typeof editorHost?.getBlockAlignState === 'function') { return editorHost.getBlockAlignState(); } if (isTextAlignmentOperation(editorHost, operation)) { return getCurrentTextAlignmentValue(editorHost, operation); } const trackedValue = getTrackedAttributeValue(editorHost, attributePath); if (typeof trackedValue !== 'undefined') { return trackedValue; } return Object.prototype.hasOwnProperty.call(operation || {}, 'unsetValue') ? operation.unsetValue : undefined; } /** * Apply one schema-backed block-attribute change to the live editor DOM. * * @param {Object|null} editorHost Active schema editor host. * @param {Object} operation Schema block-attribute operation metadata. * @param {*} value Requested attribute value. * @returns {boolean} True when the mutation was applied. */ function applyBlockAttributeChange(editorHost, operation, value, operationOptions = {}) { if (!editorHost || !operation) { return false; } const attributePath = String(operation.attribute || '').trim(); if (!attributePath) { return false; } if (attributePath === 'level') { const level = Number.parseInt(value, 10); if (!Number.isInteger(level) || level < 1 || level > 6) { return false; } if ( typeof editorHost.changeElementTag !== 'function' || typeof editorHost.setTrackedAttributeValue !== 'function' ) { return false; } // Keep the block attribute as the canonical source of truth, then // project that change into the live DOM tag for immediate feedback. editorHost.setTrackedAttributeValue(attributePath, level); editorHost.changeElementTag(`h${level}`, { validateSelection: false, saveHistory: false, }); return true; } if (operation.tagChange === true) { const tagName = String(value || '').trim().toLowerCase(); const allowedTags = Array.isArray(operation.values) ? operation.values.map(candidate => String(candidate || '').trim().toLowerCase()).filter(Boolean) : []; if (!tagName || !allowedTags.includes(tagName)) { return false; } if ( typeof editorHost.changeElementTag !== 'function' || typeof editorHost.setTrackedAttributeValue !== 'function' ) { return false; } editorHost.setTrackedAttributeValue(attributePath, tagName); editorHost.changeElementTag(tagName, { validateSelection: false, saveHistory: false, }); return true; } if (isTextAlignmentOperation(editorHost, operation)) { if (typeof editorHost.changeTextAlignment !== 'function') { return false; } const targetKey = operation.attribute === 'columnAlignment' ? 'columnAlignment' : 'textAlignment'; const textAlignmentOptions = { operation, targetKey, }; if ( operation.attribute === 'columnAlignment' && Object.prototype.hasOwnProperty.call(operationOptions || {}, 'columns') ) { textAlignmentOptions.columns = operationOptions.columns; } return editorHost.changeTextAlignment( String(value || '').trim().toLowerCase(), textAlignmentOptions ) === true; } if (attributePath === 'align') { if (typeof editorHost.changeBlockAlign !== 'function') { return false; } return editorHost.changeBlockAlign(String(value || '').trim().toLowerCase(), { operation, targetKey: 'align', }) === true; } if (typeof editorHost.setTrackedAttributeValue !== 'function') { return false; } editorHost.setTrackedAttributeValue( attributePath, normalizeBlockAttributeTrackedValue(operation, value) ); return true; } /** * Return whether one public column-alignment target payload matches the * committed runtime contract. * * The public API accepts only a zero-based index array or the `'all'` * shortcut. Scalar values intentionally fail loudly so external callers do * not depend on undocumented coercion. * * @param {*} columns Candidate explicit column target payload. * @returns {boolean} True when the payload matches the committed contract. */ function isValidExplicitColumnTarget(columns) { if (columns === 'all') { return true; } return Array.isArray(columns); } /** * Execute one or more schema-declared block-attribute changes. * * @param {Object} options Executor options. * @returns {Object|null} Applied-operation summary. */ function executeBlockAttributeOperations(options = {}) { const editorHost = options.editorHost || null; const inputOperations = Array.isArray(options.operations) ? options.operations : [ options.operation || options ]; const resolvedOperations = inputOperations .map(inputOperation => { const operationOptions = ( inputOperation && typeof inputOperation === 'object' && !Array.isArray(inputOperation) ) ? inputOperation : {}; const operation = resolveBlockAttributeOperation(editorHost, operationOptions); if (!operation) { return null; } const isExplicitColumnAlignmentOperation = ( String(operation.attribute || '').trim() === 'columnAlignment' ); const isExplicitSetColumnAlign = String(operation.id || '').trim() === 'set_column_align'; const hasColumn = Object.prototype.hasOwnProperty.call(operationOptions, 'column'); const hasColumns = Object.prototype.hasOwnProperty.call(operationOptions, 'columns'); if (isExplicitColumnAlignmentOperation && hasColumn) { return null; } if (isExplicitSetColumnAlign && !hasColumns) { return null; } if ( isExplicitColumnAlignmentOperation && hasColumns && !isValidExplicitColumnTarget(operationOptions.columns) ) { return null; } const operationValue = Object.prototype.hasOwnProperty.call(operationOptions, 'value') ? operationOptions.value : undefined; return operationValue === undefined ? null : { operation, value: operationValue, options: operationOptions, }; }) .filter(Boolean); if (!editorHost || !resolvedOperations.length) { return null; } const savedSelection = captureSelectionSnapshot(editorHost, options); const results = []; resolvedOperations.forEach(({ operation, value, options: operationOptions }) => { const hasExplicitColumnTarget = ( String(operation.attribute || '').trim() === 'columnAlignment' && Object.prototype.hasOwnProperty.call(operationOptions || {}, 'columns') ); const currentValue = hasExplicitColumnTarget ? undefined : getCurrentBlockAttributeValue(editorHost, operation); const normalizedRequestedValue = String(operation.attribute || '').trim() === 'level' ? Number.parseInt(value, 10) : value; const normalizedCurrentValue = String(operation.attribute || '').trim() === 'level' ? Number.parseInt(currentValue, 10) : currentValue; if (normalizedCurrentValue === normalizedRequestedValue) { results.push({ id: String(operation.id || '').trim(), kind: 'block_attribute_change', attribute: String(operation.attribute || '').trim(), value: normalizedRequestedValue, applied: false, }); return; } const didApply = applyBlockAttributeChange( editorHost, operation, normalizedRequestedValue, operationOptions || {} ); if (!didApply) { return; } results.push({ id: String(operation.id || '').trim(), kind: 'block_attribute_change', attribute: String(operation.attribute || '').trim(), value: normalizedRequestedValue, applied: true, }); }); if (!results.length) { return null; } restoreSelectionSnapshot(editorHost, savedSelection); if (typeof options.afterSync === 'function') { options.afterSync({ editorHost, results, operations: resolvedOperations.map(entry => entry.operation), }); } if (typeof editorHost.updateToolbarState === 'function') { editorHost.updateToolbarState(); } const historyApi = getSessionHistoryApiForHost(editorHost); if ( results.some(result => result.applied) && options.saveHistory !== false && typeof historyApi?.saveToHistory === 'function' ) { historyApi.saveToHistory(); } return { results, operationsApplied: results .filter(result => result.applied) .map(result => result.id || result.kind), attributeChanges: editorHost.attributeChanges || {}, }; } /** * Execute one schema-declared block-attribute change. * * @param {Object} options Executor options. * @returns {Object|null} Applied-operation summary. */ function executeBlockAttributeOperation(options = {}) { return executeBlockAttributeOperations({ ...options, operations: undefined, operation: options.operation || options, }); } /** * Resolve the session-owned history API for one active editor host. * * Schema operations should commit through the shared block session directly * instead of depending on MWPEditor's convenience history facade as an * integration boundary. * * @param {Object|null} editorHost Active editor host. * @returns {Object|null} Session history API, or null. */ function getSessionHistoryApiForHost(editorHost) { if (editorHost?.historyApi && typeof editorHost.historyApi === 'object') { return editorHost.historyApi; } if (typeof editorHost?.getSessionHistoryApi === 'function') { return editorHost.getSessionHistoryApi(); } const blockEditSession = editorHost?.blockEditSession || null; if (!blockEditSession || typeof blockEditSession.getHistoryApi !== 'function') { return null; } return blockEditSession.getHistoryApi( String(editorHost?.blockEditSessionScopeId || '').trim() || 'text' ); } /** * Execute one or more list operations through the canonical tracker path. * * This method intentionally disables per-operation history saves and cursor * restores inside the lower-level tracker/editors, then records one history * step after the full batch succeeds. * * @param {Object} options Executor options. * @returns {Object|null} Applied-operation summary. */ function executeListOperations(options = {}) { const listTracker = getListTrackerModule(); const editorHost = options.editorHost || null; const tracker = options.tracker || getTrackerForEditor(editorHost); const inputOperations = normalizeOperations(options); const savedSelection = captureSelectionSnapshot(editorHost, options); if ( !listTracker || typeof listTracker.applyOperation !== 'function' || typeof listTracker.getStructure !== 'function' || !tracker?.listElement || !inputOperations.length ) { return null; } const results = []; const operations = []; const appliedOperationKinds = []; for (let index = 0; index < inputOperations.length; index++) { const rawOperation = inputOperations[index]; const targetResolutionMode = getListTargetResolutionMode(options); const expandedInputs = targetResolutionMode === 'api_uuid' ? expandApiUuidListOperationInputs(rawOperation) : [ rawOperation ]; if (!expandedInputs.length) { return null; } for (let expandedIndex = 0; expandedIndex < expandedInputs.length; expandedIndex++) { const operation = buildExecutableListOperation( editorHost, tracker, expandedInputs[expandedIndex], options ); if (!operation) { return null; } const result = listTracker.applyOperation(tracker, operation, { editorHost, saveHistory: false, restoreCursor: false, }); if (!result) { return null; } operations.push(operation); results.push(result); } const appliedKind = normalizeListOperationKind(rawOperation?.kind, options); if (!appliedKind) { return null; } appliedOperationKinds.push(appliedKind); } syncEditorRoot(editorHost, tracker); restoreSelectionSnapshot(editorHost, savedSelection); if (typeof options.afterSync === 'function') { options.afterSync({ tracker, editorHost, results, operations, }); } if (typeof editorHost?.updateToolbarState === 'function') { editorHost.updateToolbarState(); } const historyApi = getSessionHistoryApiForHost(editorHost); if (options.saveHistory !== false && typeof historyApi?.saveToHistory === 'function') { historyApi.saveToHistory(); } return { results, operationsApplied: appliedOperationKinds, structure: listTracker.getStructure(tracker), }; } /** * Execute one list-type change against the currently selected list. * * @param {Object} options Executor options. * @returns {Object|null} Applied-operation summary. */ function executeCurrentListTypeChange(options = {}) { const editorHost = options.editorHost || null; const tracker = options.tracker || getTrackerForEditor(editorHost); const targetList = options.listElement || getCurrentListElement(editorHost); if (!tracker?.listElement || !targetList) { return null; } return executeListOperations({ tracker, editorHost, saveHistory: options.saveHistory !== false, afterSync: options.afterSync, operations: [ { kind: 'toggle_list_type', listPath: getListPathForElement(tracker, targetList), value: options.value, }, ], }); } SFE.SchemaOperationExecutor = { executeComponentOperation: function executeComponentOperation(options = {}) { return executeComponentOperations({ ...options, operations: undefined, operation: options.operation || options, }); }, executeComponentOperations, executeMediaOperation: function executeMediaOperation(options = {}) { return executeMediaOperations({ ...options, operations: undefined, operation: options.operation || options, }); }, executeMediaOperations, executeBlockAttributeOperation, executeBlockAttributeOperations, executeListOperations, executeCurrentListTypeChange, getTrackerForEditor, getCurrentListElement, getListPathForElement, syncEditorRoot, isUnsetBlockAttributeValue, normalizeBlockAttributeTrackedValue, getTextAlignmentCapability: getNormalizedTextAlignmentCapability, getNormalizedTextAlignmentCapability, isTextAlignmentOperation, }; })(); Leon Casino Έως 1 500 Ανά Κατάθεση! – BTX Clinical Research

Leonbet Casino Online Παιχνίδια Με Το Leon Casino

Content

Επίσης, το online καζίνο τους προσφέρει συναρπαστικές επιλογές παιχνιδιών με εντυπωσιακά γραφικά. Η τοποθεσία κυκλοφορεί από 2007 και τα μέλη του Leon Casino έχουν το μυαλό τους ήσυχο, γνωρίζοντας πως δεν χρειάζονται να ανησυχούν μη τυχόν πέσουν θύματα απάτης. Τα μέλη του Leon Casino απολαμβάνουν επίσης την ευέλικτη επιλογή ευκαιριών τυχερών παιχνιδιών καθώς και αθλητικά στοιχήματα. Για να παίξετε με πραγματικά χρήματα, θα χρειαστεί να εγγραφείτε, να δημιουργήσετε έναν προσωπικό λογαριασμό και να κάνετε την πρώτη σας κατάθεση. Επιπλέον, οι παίκτες μπορούν να επωφεληθούν από κερδοφόρα κίνητρα για συστηματική τοποθέτηση στοιχημάτων και συμμετοχή σε διάφορους διαγωνισμούς και τουρνουά. Παρέχονται επίσης μπόνους γενεθλίων στους πελάτες, τα οποία στις περισσότερες περιπτώσεις προσφέρονται με τη μορφή δωρεάν περιστροφών ή μετρητών στον προσωπικό λογαριασμό του παίκτη.

Τα γενναιόδωρα μπόνους και ανταμοιβές θα σας επιτρέψουν να γνωρίσετε καλύτερα την πλατφόρμα και να δοκιμάσετε το χέρι σας σε διάφορα παιχνίδια και τμήματα. Ένα ευρύ φάσμα μεθόδων πληρωμής διασφαλίζει ότι κάθε χρήστης μπορεί να καταθέσει και να αποσύρει με ασφάλεια τα χρήματά του. Οι κανόνες του καζίνο είναι απλοί και σαφείς και η ασφάλεια των χρηστών βρίσκεται σε εξαιρετικά υψηλό επίπεδο.” “[newline]Για να επιβεβαιώσετε τα παραπάνω, εγγραφείτε στο καζίνο, ξεκινήστε να παίζετε και κερδίστε τα πρώτα σας βραβεία https://leoncasino-gr.com/.

“leon Casino

Με λίγες κινήσεις, μπορείτε να βρείτε ακριβώς αυτό που ψάχνετε, είτε πρόκειται για το αγαπημένο σας παιχνίδι καζίνο είτε για ένα live στοίχημα στον αγαπημένο σας αθλητικό αγώνα. Με άδεια λειτουργίας από το Κουρασάο και ένα μπόνους καλωσορίσματος που φτάνει έως και τα €500 με 200 δωρεάν περιστροφές,” “το καζίνο αυτό αποτελεί εξαιρετική επιλογή για όσους αναζητούν μια νέα εμπειρία παιχνιδιού. Αξίζει να σημειωθεί ότι μόνο άτομα που έχουν συμπληρώσει την ηλικία της ενηλικίωσης επιτρέπεται να παίξουν με πραγματικά χρήματα. Για να τοποθετήσετε στοιχήματα, απαιτείται εγγραφή, δημιουργία λογαριασμού και επαλήθευση ταυτότητας. Με την ολοκλήρωση της επαλήθευσης, οι παίκτες μπορούν να απολαύσουν τα αγαπημένα τους παιχνίδια και να στοιχηματίσουν χωρίς να ανησυχούν για την ασφάλεια.

Η δυνατότητα αναζήτησης παιχνιδιών με βάση τον πάροχο λογισμικού είναι μόνο μία από τις πολλές λειτουργίες που κάνουν την εμπειρία σας μοναδική. Ο στοιχηματισμός ατόμων κάτω των eighteen ετών είναι παράνομος και επομένως η δημιουργία λογαριασμού απαγορεύεται σε ανηλίκους στον ιστότοπο μας. Ενθαρρύνουμε τους γονείς και τους ενήλικες να επιβλέπουν τη χρήση του Διαδικτύου από τα παιδιά και να τους αποτρέπουν από την πρόσβαση σε ιστότοπους τυχερών παιχνιδιών. Ο όρος ‘γκανιότα’ χρησιμοποιείται για να περιγράψει την τάση των αποτελεσμάτων σε ένα συγκεκριμένο αθλητικό γεγονός. Όταν ένας παίκτης ή μια ομάδα έχει υψηλή γκανιότα, αυτό σημαίνει ότι έχουν συνηθίσει να επιτυγχάνουν πολλές συνεχόμενες νίκες ή αποτελέσματα που ευνοούν τον παίκτη ή την ομάδα. Επιλέξτε τη μέθοδο που σας ταιριάζει καλύτερα και απολαύστε την εμπειρία σας παίζοντας τα αγαπημένα σας παιχνίδια τζόγου όλο το 24ωρο.

Γενναιόδωρα Μπόνους Από Το Leon Casino

Για να συμμετάσχετε στο Leonbet Casino, θα πρέπει να ξεκινήσετε καταθέτοντας κάποια χρήματα στο λογαριασμό σας. Όλοι οι παίκτες μπορούν να καταθέσουν μόλις 10€ στο Leonbet Casino, με το ίδιο ποσό να μπορεί να γίνει ανάλυψη μέσα σε λίγες ώρες. Οι επιλογές μπορεί να διαφέρουν ανάλογα με τη χώρα από την οποία προέρχεστε, αλλά συνήθως θα βρείτε δημοφιλείς επιλογές όπως Visa, Mastercard, Bitcoin, Ethereum, Tether, και άλλες. Boomerang Casino Το Boomerang casino έκανε την είσοδό του στον κόσμο των διαδικτυακών καζίνο το 2020 και από τότε έχει κερδίσει τις εντυπώσεις των παικτών με την ευρεία ποικιλία παιχνιδιών και τα πλούσια μπόνους. Μη διστάσετε να περιστρέψετε τους κυλίνδρους των αγαπημένων σας κουλοχέρηδων από Παρασκευή έως Κυριακή και λάβετε επιστροφή μετρητών 10% σε όλα τα χαμένα στοιχήματα κάθε Δευτέρα.

Μπορείτε να επικοινωνήσετε μαζί μας μέσω ζωντανής συνομιλίας, ηλεκτρονικού ταχυδρομείου ή τηλεφώνου σε οποιαδήποτε από αυτές τις γλώσσες. Η υποστήριξη πελατών της Leonbet είναι διαθέσιμη 24/7 μέσω live chat, τηλεφωνικής υποστήριξης και email. Οι κάτοχοι device που βασίζονται σε iOS μπορούν να δημιουργήσουν μόνο ένα εικονίδιο με γρήγορη πρόσβαση στην πύλη. Ο κατάλογος συνεχίζεται με προμηθευτές μικρότερης κλίμακας, αλλά παρόλα αυτά πολύ σεβαστούς, όπως οι Playson, Endorphina, NoLimit Town, Wazdan, Bgaming, Gamzix, Hacksaw, Spribe και πολλοί άλλοι. Οι φανταστικές μας προσφορές και η ευρεία γκάμα μεθόδων πληρωμής προσφέρουν ευκολία και ικανοποίηση σε όλους τους παίκτες, ανεξαρτήτως επιπέδου.” “[newline]Τέλος, το καζίνο μας, πρωτοπορώντας, διαθέτει δική του Android application, για να απολαμβάνετε παιχνίδι παντού και πάντα. Ταυτόχρονα είμαστε εδώ για όλες σας τις ανάγκες και απορίες, παρέχοντάς σας 24/7 υποστήριξη μέσω email και live chat.

Συνδρομή Και Υποστήριξη Του Leon Casino

Και για όσους απολαμβάνουν τα αγαπημένα τους slots, το 10% Καθημερινό Cashback είναι εδώ για να σας στηρίζει. Με μια καθημερινή επιστροφή μετρητών στις απώλειές σας, δεν θα σταματήσετε να παίζετε και να απολαμβάνετε το παιχνίδι σας χωρίς ανησυχία. Τοποθετήστε τον πονταρισμό σας αμέσως και, εάν επιτύχετε, εξαργυρώστε τα καθαρά κέρδη σας. Επαναλάβετε αυτήν τη διαδικασία έως και 20 φορές για να αποκτήσετε τα 20 δωρεάν στοιχήματα σε αυτήν την εκπληκτική προσφορά.

Είναι επόμενο λοιπόν να κατακλυστείτε από παιχνίδια κάθε είδους, σε σημείο που να μην ξέρετε ποιό να διαλέξετε. Με τη λήψη της εφαρμογής στην κινητή τους συσκευή, οι χρήστες αποκτούν πρόσβαση στο Dual Bonus – βίντεο πόκερ. Αυτό το παιχνίδι σας επιτρέπει να κερδίσετε πραγματικά χρήματα σε σύντομο χρονικό διάστημα και είναι κατάλληλο ακόμη και για αρχάριους. Ειδικά για νέους παίκτες, το καζίνο προσφέρει ένα ελκυστικό μπόνους εγγραφής που θα σας δώσει την ώθηση να ξεκινήσετε δυναμικά.

Είναι Νόμιμο Και Ασφαλές Το Leonbet Casino Στην Ελλάδα;

Είχα την ευκαιρία να στοιχηματίσω σε πολλά αθλητικά γεγονότα και να απολαύσω τα καζίνο παιχνίδια τους. Με άδεια από τις αξιόπιστες αρχές του Κουρασάο και του Καναβάκε, το Leon Casino εγγυάται ένα ασφαλές” “και δίκαιο περιβάλλον παιχνιδιού. Η υπερσύγχρονη τεχνολογία κρυπτογράφησης μας εξασφαλίζει ότι οι προσωπικές και οικονομικές σου πληροφορίες είναι προστατευμένες ανά πάσα στιγμή. Αφού συνδεθείτε στο καζίνο, το κουμπί υπολοίπου και κατάθεσης εμφανίζεται στην επάνω δεξιά γωνία, με μια μπάρα προόδου που παρακολουθεί πότε ο επόμενος κωδικός κουπονιού θα φτάσει στο λογαριασμό.

  • Μπορείτε επίσης να παίξετε εκδόσεις επίδειξης, να τοποθετήσετε πραγματικά στοιχήματα και να αποσύρετε τα κέρδη σας μέσω του κατάλληλου συστήματος πληρωμών.
  • Παρέχονται επίσης μπόνους γενεθλίων στους πελάτες, τα οποία στις περισσότερες περιπτώσεις προσφέρονται με τη μορφή δωρεάν περιστροφών ή μετρητών στον προσωπικό λογαριασμό του παίκτη.
  • Κάθε εβδομάδα από Παρασκευή έως Κυριακή, σας δίνουμε μπόνους 50% στην επόμενη κατάθεσή σας έως και 110 €.

Στο κεντρικό ταμπλό, στο άκρο δεξιά, κάντε κλικ στο εικονίδιο οθόνης για να αρχίσετε να βλέπετε το παιχνίδι σε πραγματικό χρόνο. Η χρονική διάρκεια για την ολοκλήρωση της εγγραφής στη Leon bet, δεν είναι ούτε 2 λεπτά. Μια βασική προϋπόθεση για την εγγραφή σας αποτελεί το να είστε ενήλικες (άνω των 18 ετων). Κάντε την πρώτη σας κατάθεση και απολαύστε ένα εξαιρετικό μπόνους εγγραφής από την Leonbet. Το casino Leon GR διαθέτει μια εξειδικευμένη υπηρεσία υποστήριξης που είναι διαθέσιμη 24/7.

Οι Προσφορές Και Τα Μπόνους Μας

Με αυτόν τον τρόπο, μπορείτε να πάρετε μια μερική επιστροφή του πονταρίσματός σας, αν νιώθετε ότι το στοίχημά σας δεν πηγαίνει καλά. Εάν είστε ένα ενεργό άτομο που είναι συνεχώς σε κίνηση, αλλά δεν μπορείτε να φανταστείτε τη ζωή σας χωρίς τζόγο, μπορείτε να κατεβάσετε την εφαρμογή του διαδικτυακού καζίνο στην κινητή σας συσκευή. Το πρόγραμμα είναι προσαρμοσμένο για τηλέφωνα με λειτουργικά συστήματα iOS και Android, επιτρέποντάς σας να έχετε πάντα πρόσβαση στους αγαπημένους σας κουλοχέρηδες. Αυτό το ειδικό event σας προσκαλεί να ανακαλύψετε κρυμμένους θησαυρούς και να ζήσετε τη συγκίνηση του παιχνιδιού με εντελώς δωρεάν περιστροφές. Δηλώστε συμμετοχή και αποκτήστε 12 δωρεάν περιστροφές στο συναρπαστικό παιχνίδι OOF The Goldmine World.

  • Το λόμπι του καζίνο είναι σχεδιασμένο με εμπειρογνωμοσύνη, με ένα μεγάλο κεντρικό πλαίσιο αναζήτησης και ένα αναπτυσσόμενο μενού που εμφανίζει πάνω από 120 παρόχους λογισμικού.
  • Αφού συνδεθείτε στο καζίνο, το κουμπί υπολοίπου και κατάθεσης εμφανίζεται στην επάνω δεξιά γωνία, με μια μπάρα προόδου που παρακολουθεί πότε ο επόμενος κωδικός κουπονιού θα φτάσει στο λογαριασμό.
  • Με πλούσια συλλογή παιχνιδιών όπως κουλοχέρηδες, επιτραπέζια και live καζίνο, η εφαρμογή παρέχει γρήγορη και ομαλή λειτουργία με εντυπωσιακά γραφικά και ασφαλείς συναλλαγές.
  • Με στόχο την υπευθυνότητα, ενθαρρύνουμε τους παίκτες να παίζουν με μέτρο και να απολαμβάνουν το παιχνίδι τους χωρίς να τίθεται σε κίνδυνο η χρηματική τους ασφάλεια.
  • Οι κουλοχέρηδες προσφέρονται τόσο σε εκδόσεις επίδειξης όσο και για παιχνίδι με πραγματική κατάθεση.

Συνεργαζόμαστε με τους κορυφαίους παρόχους λογισμικού στον κόσμο για να σας προσφέρουμε την καλύτερη δυνατή εμπειρία παιχνιδιού. Η βιβλιοθήκη παιχνιδιών μας είναι τόσο πλούσια που θα χρειαστείτε μέρες για να την εξερευνήσετε όλη. Το πρόγραμμά μας αφοσίωσης, σχεδιασμένο ειδικά για τους πιο πιστούς μας παίκτες, σας προσκαλεί σε έναν κόσμο όπου κάθε στοιχηματισμός σας μετατρέπεται σε πολύτιμους “”Λέοντες””. Αυτά τα ψηφιακά νομίσματα είναι ο κλειδάριθμος για να ξεκλειδώσετε έναν θησαυρό από αποκλειστικά μπόνους, όπως χρηματικά έπαθλα, δωρεάν περιστροφές και πολλά άλλα.

Επιλέξτε Τα Καλύτερα Παιχνίδια Στο Leon

Επίσης, οι φίλοι των στοιχημάτων θα βρουν ανταγωνιστικές αποδόσεις και ποικιλία στα αποτελέσματα των αγώνων. Στον διαρκώς εξελισσόμενο κόσμο του διαδικτυακού τζόγου, το Leon Casino αποτελεί την ιδανική επιλογή για κάθε παίκτη που αναζητά αξιοπιστία και ασφάλεια. Από το 2007, το καζίνο μας έχει χτίσει μια σταθερή φήμη, προσφέροντας ένα περιβάλλον παιχνιδιού όπου η δικαιοσύνη και η προστασία των παικτών αποτελούν απόλυτη προτεραιότητα. Επισκέψου ξανά την κομψότητα των κλασικών καζίνο με μια πολύ μεγάλη επιλογή επιτραπέζιων παιχνιδιών. Είτε είναι τα στρατηγικά βάθη του μπλακτζάκ, η περιστρεφόμενη γοητεία της ρουλέτας, είτε του μπακαρά για τον ενθουσιασμό που σε εμπνέει, από τα αυτονόητα έως τα εμπνευσμένα, τα τραπέζια μας ταιριάζουν σε κάθε διάθεση και στυλ.

Οι επισκέπτες της εικονικής πλατφόρμας έχουν επίσης την ευκαιρία να δοκιμάσουν την τύχη τους σε ζωντανά παιχνίδια. Οι προγραμματιστές της πλατφόρμας ενδιαφέρονται για την ασφάλεια των πελατών τους, όπως αποδεικνύεται από πολλές θετικές κριτικές χρηστών. Είμαστε σίγουροι ότι τα επιτραπέζια παιχνίδια μας θα καταλάβουν μία ξεχωριστή θέση στην καρδιά σας και θα σας προσφέρουν ατελείωτες ώρες διασκέδασης και στρατηγικής. Από την κλασική ρουλέτα μέχρι το συναρπαστικό μπλάκτζακ και το επιβλητικό πόκερ, η ποικιλία των τραπεζιών θα ικανοποιήσει κάθε παίκτη που αγαπά τη στρατηγική και την πρόκληση.

Leonbet Στοιχηματα

Οι αφοσιωμένοι και τακτικοί παίκτες μας έχουν ένα ακόμη μπόνους, όπως ένα πρόγραμμα VIP. Τα πλεονεκτήματά σας περιλαμβάνουν αποκλειστικά οφέλη,” “όπως εξατομικευμένες προσφορές, υψηλότερα όρια καταθέσεων και εξόδων και τον αποκλειστικό διαχειριστή του λογαριασμού σας. Πιο συγκεκριμένα, το πρόγραμμα Cashback στο Skyward προσφέρει επίσης 10% επιστροφή μετρητών έως και 500€ για όλους τους λάτρεις του Skyward, κρατώντας τον ενθουσιασμό σας στο απόγειο. Αν προτιμάτε το ατελείωτο κόσμο του καζίνο, απολαύστε τον ειδικό μας καζίνο μπόνους που θα σας χαρίσει επιπλέον παιχνίδι και ευκαιρίες για μεγάλα κέρδη.

  • Μια βασική προϋπόθεση για την εγγραφή σας αποτελεί το να είστε ενήλικες (άνω των 18 ετων).
  • Καθώς ανεβαίνετε επίπεδα, από τον “”Αρχάριο”” στον “”Θρύλο””, αποκτάτε πρόσβαση σε έναν κόσμο προνομίων που θα σας κόψει την ανάσα.
  • Αξίζει να σημειωθεί ότι μόνο άτομα που έχουν συμπληρώσει την ηλικία της ενηλικίωσης επιτρέπεται να παίξουν με πραγματικά χρήματα.
  • Οι φανταστικές μας προσφορές και η ευρεία γκάμα μεθόδων πληρωμής προσφέρουν ευκολία και ικανοποίηση σε όλους τους παίκτες, ανεξαρτήτως επιπέδου.” “[newline]Τέλος, το καζίνο μας, πρωτοπορώντας, διαθέτει δική του Android application, για να απολαμβάνετε παιχνίδι παντού και πάντα.

Μπορείτε να επικοινωνήσετε με τους χειριστές μέσω της εικονικής συνομιλίας στον ιστότοπο ή να στείλετε email στο [email protected]. Επιπλέον, μπορείτε να ενημερώνεστε για τις τελευταίες καινοτομίες μέσω του επίσημου καναλιού στο Telegram. Για την κατάθεση, μπορείτε να χρησιμοποιήσετε το πορτοφόλι Piastrix, κάρτες Visa και άλλες μεθόδους. Συζητάμε για το κορυφαίο casino στη χώρα, επομένως φυσικά θα είχε αποκλειστικές εφαρμογές για Android, Huawei και iOS συσκευές. Αν προτιμάτε να παίξετε από το κινητό σας χωρίς να κατεβάζετε και εγκαθιστάτε εφαρμογές, απλά ανοίξτε τον browser σας και εισάγετε το URL του leonbet. com. gr.

Έκδοση Για Κινητά

Η γκάμα των παιχνιδιών” “στην εν λόγω επιχείρηση τυχερών παιχνιδιών είναι τόσο διαφορετική που ακόμη και ο πιο απαιτητικός πελάτης θα μπορέσει να επιλέξει την κατάλληλη επιλογή ψυχαγωγίας. Εδώ, οι παίκτες έχουν τη δυνατότητα να δοκιμάσουν την τύχη τους σε παιχνίδια διαφόρων κατηγοριών, και μπορούν να παίξουν ανά πάσα στιγμή χρησιμοποιώντας οποιαδήποτε συσκευή. Το Leon casino προσφέρει πιστοποιημένους κουλοχέρηδες και ένα νόμιμο αθλητικό βιβλίο για στοιχήματα. Με τη διεθνή άδεια από το Κουρασάο, οι παίκτες μπορούν να αναμένουν δίκαιες και ασφαλείς συνεδρίες τυχερών παιχνιδιών. Τα προηγμένα πρωτόκολλα κρυπτογράφησης εξασφαλίζουν την προστασία των προσωπικών δεδομένων και τη διασφάλιση ασφαλών πληρωμών. Ο κατάλογος του καζίνο περιλαμβάνει πάνω από 4000 κουλοχέρηδες, ζωντανά παιχνίδια με πραγματικούς οικοδεσπότες και παιχνίδια crash.

  • Η γρήγορη φόρτωση και η ομαλή πλοήγηση επιτρέπουν στους χρήστες να απολαμβάνουν το αγαπημένο τους καζίνο χωρίς καθυστερήσεις, είτε πρόκειται για παιχνίδια είτε για συναλλαγές.
  • Κουλοχέρηδες (Megaways, Declines & Wins), Reside casino, Live shows, Επιτραπέζια παιχνίδια, Στιγμιαία παιχνίδια, Αθλητικό στοίχημα, e-sports, V-sports, κ. α.
  • Μη διστάσετε να περιστρέψετε τους κυλίνδρους των αγαπημένων σας κουλοχέρηδων από Παρασκευή έως Κυριακή και λάβετε επιστροφή μετρητών 10% σε όλα τα χαμένα στοιχήματα κάθε Δευτέρα.
  • Δημιουργήστε έναν λογαριασμό στο Leon και αποκτήστε έως και 1. five hundred € για να απολαύσετε σε παιχνίδια καζίνο.

Οι κουλοχέρηδες βίντεο στο λεον καζινο είναι η πιο δημοφιλής επιλογή για τους παίκτες, καθώς προσελκύουν το κοινό με τη μεγάλη ποικιλία θεμάτων και τα ενσωματωμένα μπόνους. Μπορείτε να δοκιμάσετε παιχνίδια που σχετίζονται με ζώα, αρχαίες πόλεις ή μαγειρική. Έχουν δημιουργήσει μια σελίδα FAQ για να αντιμετωπίσουν τα πιο συνηθισμένα ζητήματα.

Ερωτήσεις; Επικοινωνήστε Με Την Υποστήριξη!

Οι παίκτες μπορούν να βρουν μια ποικιλία μηχανικών κουλοχέρηδων, όπως τα Hold & Win και Megaways. Επιπλέον, έχετε τη δυνατότητα να αγοράσετε γύρους μπόνους εντός των παιχνιδιών, προσφέροντας σας την ευκαιρία να πολλαπλασιάσετε τα κέρδη σας μέσω του ενσωματωμένου τροχού της τύχης. Διαλέξαμε μερικά παιχνίδια με live supplier για να τα εξετάσουμε και διαπιστώσαμε ότι η ποιότητα είναι εξαιρετική. Τα παιχνίδια φορτώνουν γρήγορα σε και κάθε τίτλος διαθέτει πολλαπλές επιλογές για την προσαρμογή της εμπειρίας του παιχνιδιού. Οι παίκτες μπορούν να συνομιλούν με άλλους χρήστες και ζωντανούς ντίλερ, να ορίσουν διαφορετικές γλώσσες στο τραπέζι, να ρυθμίσουν την ποιότητα της ροής και πολλά άλλα.

  • Στον διαρκώς εξελισσόμενο κόσμο του διαδικτυακού τζόγου, το Leon Casino αποτελεί την ιδανική επιλογή για κάθε παίκτη που αναζητά αξιοπιστία και ασφάλεια.
  • Είμαστε εδώ για να σας προσφέρουμε την καλύτερη εμπειρία και να σας κάνουμε να νιώσετε σαν να βρίσκεστε σε πραγματικό καζίνο.
  • Είμαστε σίγουροι ότι τα επιτραπέζια παιχνίδια μας θα καταλάβουν μία ξεχωριστή θέση στην καρδιά σας και θα σας προσφέρουν ατελείωτες ώρες διασκέδασης και στρατηγικής.
  • Οι διαθέσιμες μέθοδοι περιλαμβάνουν πιστωτικές/χρεωστικές κάρτες, e-wallets όπως Skrill και Neteller, προπληρωμένες κάρτες και κουπόνια, καθώς και διάφορα κρυπτονομίσματα.
  • Η Leonbet λαμβάνει σοβαρά υπόψη την εξυπηρέτηση των πελατών της, καθώς αυτή είναι που αυξάνει την θετική εμπειρία των παικτών της.

Κάντε εγγραφή και επωφεληθείτε από το μπόνους καλωσορίσματος, δωρεάν περιστροφές και πολλές άλλες εκπλήξεις της Leon guess. Βούτα στην καρδιά της συγκίνησης με την αξεπέραστη επιλογή παιχνιδιών καζίνο του Leon Casino. Σε κάθε παιχνίδι, είτε κάνεις μια ριψοκίνδυνη περιστροφή των τροχών στον αλγόριθμο είτε κινείσαι με στρατηγικές και με προσδοκίες ζωντανού παιχνιδιού, με το χαρτοφυλάκιό μας ωστόσο μπορείς να βρεις κάτι για κάθε κλασικό παίκτη. Τα παιχνίδια μας αντιπροσωπεύουν όχι μόνο τις νίκες, αλλά και την πορεία του έμπειρου παιχνιδιού.

Πληρωμές Στο Καζίνο Leon

Έχουν χωρίσει το τμήμα ζωντανών παιχνιδιών σε κατηγορίες για Ρουλέτα, Μπλάκτζακ, Drop & Wins, Game Indicates, Μπακαρά και Πόκερ, ενώ ένα πλαίσιο αναζήτησης καθιστά εύκολη τη” “γρήγορη εύρεση οποιουδήποτε ζωντανού παιχνιδιού. Η διεπαφή της πύλης είναι διαθέσιμη σε nine γλώσσες, συμπεριλαμβανομένων των ελληνικών. Οι χρήστες που κατοικούν στην Ελλάδα μπορούν να επισκεφθούν την επίσημη πύλη, να εγγραφούν και να απολαύσουν βασικές λειτουργίες. Μπορείτε να επιλέξετε ένα νόμισμα όταν δημιουργείτε έναν λογαριασμό, για παράδειγμα, EUR. Οι παίκτες στην Ελλάδα έχουν ακόλουθους τρόπους να κάνουν τις καταθέσεις και τις αναλήψεις του στο Leon Casino. Κουλοχέρηδες (slots), Επιτραπέζια παιχνίδια, Ζωντανό καζίνο, Concert events, Στοιχηματικά, Live gambling bets, V-sports, e-sports.”

  • Αυτό σημαίνει ότι ορισμένες προσφορές είναι ειδικά σχεδιασμένες για το τμήμα των αθλητικών στοιχημάτων και ενδέχεται να μην ισχύουν για τα παιχνίδια καζίνο.
  • Αυτό το ειδικό event σας προσκαλεί να ανακαλύψετε κρυμμένους θησαυρούς και να ζήσετε τη συγκίνηση του παιχνιδιού με εντελώς δωρεάν περιστροφές.
  • Αυτά τα μηχανήματα αναπτύσσονται από γνωστούς παρόχους και προσελκύουν την προσοχή ενός ευρέος κοινού παγκοσμίως, συμπεριλαμβανομένης της Ελλάδας.
  • Η πλοήγηση μέσω της έκδοσης για κινητά είναι πολύ βολική και απλή, μπορείτε εύκολα να βρείτε την ενότητα που χρειάζεστε και να κατανοήσετε γρήγορα ολόκληρη την εφαρμογή.
  • Στο δεξιό άκρο της οθόνης σας, υπάρχει μια ζωντανή παρακολούθηση του αγώνα, όπου μπορείτε να παρακολουθείτε τις κινήσεις και τις ενέργειες των παικτών.

Στη μέση της οθόνης, έχουν δημιουργηθεί κατηγορίες παιχνιδιών για τα Αγαπημένα, Δημοφιλή, Νέα, Χειμερινά, Κουλοχέρηδες, Bonus Buy, Fall & Wins, Μπλάκτζακ, Ρουλέτα και Γρήγορα παιχνίδια. Μόλις δειτε τις επιλογές κουλοχέρηδων στο Leon Casino, δεν θα θελήσετε να επισκεφθείτε άλλο online καζίνο. Μπορείτε να επιλέξετε παιχνίδια από τους αγαπημένους σας παρόχους ή να εστιάσετε σε νέους ή δημοφιλείς κουλοχέρηδες. Στο τμήμα του καζίνο, μπορείτε να απολαύσετε ένα συναρπαστικό μπόνους καλωσορίσματος, επιστροφή χρημάτων για τις απώλειές σας, ειδικές προσφορές για συγκεκριμένα παιχνίδια και ένα μοναδικό πρόγραμμα επιβράβευσης. Στο ζωντανό καζίνο της Leon, έχετε τη δυνατότητα να επικοινωνήσετε σε πραγματικό χρόνο με τους dealers. Κάντε ελεύθερα τα σχόλια σας, και ζητήστε πληροφορίες, πάντα με σεβασμό προς τους κανονισμούς που ισχύουν.

Leon Casino Greece: Κουλοχέρηδες Και Νόμιμα Στοιχήματα

Πληροφορίες σχετικά με την κατάσταση του λογαριασμού σας θα εμφανιστούν αμέσως στον προσωπικό σας λογαριασμό. Η εφαρμογή μας για Android είναι σχεδιασμένη να σας προσφέρει μία δυνατή δόση δράσης, ακόμα και όταν βρίσκεστε εν κινήσει. Η γρήγορη φόρτωση και η ομαλή πλοήγηση επιτρέπουν στους χρήστες να απολαμβάνουν το αγαπημένο τους καζίνο χωρίς καθυστερήσεις, είτε πρόκειται για παιχνίδια είτε για συναλλαγές.

  • Μπορείτε να αποδεχθείτε το μπόνους απευθείας μέσα στην εφαρμογή, επισκεπτόμενοι την ενότητα «Μπόνους».
  • Το Boomerang casino έκανε την είσοδό του στον κόσμο των διαδικτυακών καζίνο το 2020 και από τότε έχει κερδίσει τις εντυπώσεις των παικτών με την ευρεία ποικιλία παιχνιδιών και τα πλούσια μπόνους.
  • H Leon Bet ιδρύθηκε το 2007 και μέσα σε σύντομο χρονικό διάστημα έχει καταφέρει να τραβήξει το ενδιαφέρον πολλών παικτών από διάφορες χώρες όπως την Ελλάδα.
  • Η υποστήριξη πελατών της Leonbet είναι διαθέσιμη 24/7 μέσω live chat, τηλεφωνικής υποστήριξης και e mail.
  • Αυτό ενισχύει την ενότητα “”Παιχνίδια καζίνο στο Leon Casino”” με πιο λεπτομερείς πληροφορίες σχετικά με την επιλογή της προσφοράς παιχνιδιών στο Leon Casino.

Με μία ασφαλή άδεια από το Curaçao, το καζίνο μας μπορεί να υπερηφανεύεται για την τεράστια γκάμα παιχνιδιών και τις συναρπαστικές προσφορές προς τους παίκτες του, οι οποίοι το ακολουθούν φανατικά. Η διεπαφή της εφαρμογής είναι απλή και κατανοητή, ώστε ακόμα και οι αρχάριοι να μπορούν εύκολα να κατανοήσουν τις δυνατότητες χρήσης της. Η εφαρμογή προσφέρει την ίδια γκάμα παιχνιδιών που θα βρείτε και στην επίσημη ιστοσελίδα του καζίνο.

Leon Casino Το Πιο Αριστοκρατικό Καζίνο Για Τους Έλληνες Παίκτες (

Μπορείτε επίσης να παίξετε εκδόσεις επίδειξης, να τοποθετήσετε πραγματικά στοιχήματα και να αποσύρετε τα κέρδη σας μέσω του κατάλληλου συστήματος πληρωμών. Το gambling establishment Leon προσφέρει στους χρήστες μια μεγάλη ποικιλία από διαφορετικούς τύπους παιχνιδιών. Εδώ, οι λάτρεις του τζόγου θα βρουν δημοφιλείς κουλοχέρηδες βίντεο, παιχνίδια με κάρτες, επιτραπέζια παιχνίδια, παιχνίδια crash και λοταρίες. Το να δίνετε τα δεδομένα σας σε ιστότοπους του Διαδικτύου μπορεί να είναι δύσκολο, διότι πώς ξέρετε αν μπορείτε να τους εμπιστευτείτε; Αυτός είναι ο λόγος για τον οποίο η προτεραιότητά μας είναι η ασφάλειά σας. Χρησιμοποιούμε την πιο πρόσφατη τεχνολογία κρυπτογράφησης για την προστασία και την ασφάλεια των προσωπικών σας στοιχείων.

  • Η διεπαφή της εφαρμογής είναι απλή και κατανοητή, ώστε ακόμα και οι αρχάριοι να μπορούν εύκολα να κατανοήσουν τις δυνατότητες χρήσης της.
  • Ολοκληρώστε τη διαδικασία σύνδεσης στο Leon Casino σήμερα, κάντε την πρώτη σας κατάθεση και εμείς θα την αντιστοιχίσουμε 100% μέχρι και 200 λίρες.
  • Από το 2007, το καζίνο μας έχει χτίσει μια σταθερή φήμη, προσφέροντας ένα περιβάλλον παιχνιδιού όπου η δικαιοσύνη και η προστασία των παικτών αποτελούν απόλυτη προτεραιότητα.
  • Για την κατάθεση, μπορείτε να χρησιμοποιήσετε το πορτοφόλι Piastrix, κάρτες Visa και άλλες μεθόδους.

Είμαστε έτσι δίπλα σας σε κάθε σας παιχνίδι, επιστρέφοντας ένα μέρος των χρημάτων σας για να συνεχίσετε τη διασκέδαση. Οι υπόλοιπο παίκτες μπορούν να αξιολογήσουν τη κινητή έκδοση του ιστότοπου, ο οποίος είναι πλήρως συμβατός με κάθε είδους συσκευής ανεξαρτήτως της πλατφόρμας πάνω στην οποία δουλεύει. Το Leon Casino σας έχει φροντίσει και σε αυτό” “τον τομέα έχοντας δημιουργήσει δική του εφαρμογή για κινητά Android. Μεταβείτε στην ιστοσελίδα μας για να κατεβάσετε το Leon Casino App και να παίζετε τα παιχνίδια μας στην οθόνη του κινητού σας. Εκεί μπορείτε να παρακολουθείτε τις οικονομικές συναλλαγές σας, να ενημερώνεστε για τα τελευταία νέα και να λαμβάνετε χρήσιμες πληροφορίες για να απολαύσετε ένα ενδιαφέρον και κερδοφόρο παιχνίδι.

Leonbet Καζίνο Rtp & Πληρωμές

Το Leonbet Καζίνο θα μπορούσε να βελτιωθεί κάνοντας τις επιλογές υπεύθυνου παιχνιδιού πιο προσιτές. Αξίζει επίσης να δοκιμάσετε παιχνίδια τζάκποτ από την Playson, όπως το Noble Coins Hold and Win και το Vikings Gods – που έρχεται με 97% RTP. Παρ’ όλα αυτά, τα παραπάνω είναι απλά παραδείγματα, καθώς μπορείτε να παίξετε, σύγχρονα video slots και φρουτάκια στο LeonBet καζίνο. Το Cash Out είναι μια δημοφιλής επιλογή που σας δίνει τη δυνατότητα να κλείσετε το στοίχημά σας πριν από” “το τέλος του αγώνα.

  • Ιδιαίτερα το μπόνους καλωσορίσματος, το οποίο παρέχεται στο αρχικό στάδιο του παιχνιδιού, αυξάνει σημαντικά τις πιθανότητες νίκης.
  • Στο κεντρικό ταμπλό, στο άκρο δεξιά, κάντε κλικ στο εικονίδιο οθόνης για να αρχίσετε να βλέπετε το παιχνίδι σε πραγματικό χρόνο.
  • Με τέτοια πληρότητα σε όλους τους τομείς, δεν είναι τυχαία η αναγνώρισή του από τους παίκτες διεθνώς ως ένα από τα καλύτερα καζίνο στον κόσμο.
  • Επιπλέον, μπορείτε να ενημερώνεστε για τις τελευταίες καινοτομίες μέσω του επίσημου καναλιού στο Telegram.
  • Μέσα στο παράθυρο του παιχνιδιού υπάρχει λειτουργία συνομιλίας που σας επιτρέπει να επικοινωνείτε με άλλους συμμετέχοντες και τον” “οικοδεσπότη.

Παρά την εντυπωσιακή εμφάνιση, η λειτουργικότητα παραμένει το επίκεντρο της πλατφόρμας, με τις κατηγορίες των παιχνιδιών και τις προσφορές να είναι ευδιάκριτες και εύκολα προσβάσιμες από την αρχική σελίδα. Για να ανοίξετε έναν λογαριασμό στη Leonbet, μεταβείτε στην επίσημη ιστοσελίδα τους και επιλέξτε την επιλογή “Εγγραφή”. Συμπληρώστε τα απαιτούμενα προσωπικά στοιχεία και ακολουθήστε τα βήματα για την επιβεβαίωση του λογαριασμού σας. Είναι σημαντικό για μένα να έχω την επιλογή να στοιχηματίσω σε διάφορα αθλήματα και παιχνίδια, και αυτός είναι ο λόγος που την προτιμώ.

Καζίνο Leon

Ολοκληρώστε τη διαδικασία σύνδεσης στο Leon Casino σήμερα, κάντε την πρώτη σας κατάθεση και εμείς θα την αντιστοιχίσουμε 100% μέχρι και 200 λίρες. Δεν είναι θαυμάσιο να έχετε ένα διπλάσιο ποσό χρημάτων για να παίξετε; Οι υπόλοιπες προσφορές μας, οι οποίες αλλάζουν κάθε μέρα, σας περιμένουν επίσης στην ιστοσελίδα μας. Επισκεπτόμενοι την ενότητα Betgames στο Leon casino Ελλαδα, οι πελάτες έχουν τη” “δυνατότητα να απολαύσουν accident παιχνίδια με γρήγορες νίκες, καθώς και μια ποικιλία επιτραπέζιων παιχνιδιών με πραγματικούς παρουσιαστές. Ορισμένες από τις διαθέσιμες κυκλοφορίες περιλαμβάνουν το Skyward, την κλασική ρουλέτα, το Penalty Traditional και άλλες. Για να ξεκινήσετε την online ψυχαγωγία, θα χρειαστεί να δημιουργήσετε έναν λογαριασμό και να συνδεθείτε στον προσωπικό σας προφίλ.

  • Για τους πιο τολμηρούς, υπάρχουν παιχνίδια με γρήγορους γύρους, που προσφέρουν υψηλά επίπεδα κινδύνου και τη δυνατότητα γρήγορης νίκης.
  • Οι προγραμματιστές της πλατφόρμας ενδιαφέρονται για την ασφάλεια των πελατών τους, όπως αποδεικνύεται από πολλές θετικές κριτικές χρηστών.
  • Με μια γρήγορη ματιά στην ιστοσελίδα της Leonbet μπορούμε να διαπιστώσουμε την εξαιρετική σχεδίαση της, καθώς στην σχεδίαση της έχουν χρησιμοποιηθεί οι τελευταίες τεχνολογίες του website design.
  • Μπορείτε να παίξετε σε διαδικτυακά καζίνο από το κινητό σας είτε μέσω προγράμματος περιήγησης είτε μέσω ειδικής εφαρμογής.
  • Το κύριο πλεονέκτημα είναι μια ξεχωριστή εφαρμογή για κινητά, η οποία είναι τόσο εύκολη στη χρήση όσο η έκδοση ιστού.

Σας καλούμε να γίνετε μέρος της κοινότητάς μας και να απολαύσετε ό, τι καλύτερο έχει να προσφέρει το διαδικτυακό παιχνίδι. Έχουμε φροντίσει επίσης η πλατφόρμα μας να είναι διαθέσιμη σε πολλές γλώσσες, ώστε παίκτες από όλο τον κόσμο να μπορούν να απολαύσουν την αγαπημένη τους διασκέδαση. Επιπλέον, η δυνατότητα επιλογής μεταξύ λειτουργίας ημέρας και νύχτας εξασφαλίζει ότι μπορείτε να παίξετε ανά πάσα στιγμή και από οπουδήποτε, με τον πιο άνετο τρόπο.

Leave a Reply

Your email address will not be published. Required fields are marked *


Fatal error: Uncaught Error: Interface "WP_Rocket\Engine\Optimization\Minify\ProcessorInterface" not found in /home/575368.cloudwaysapps.com/hhchdnqmxq/public_html/wp-content/plugins/wp-rocket/inc/Engine/Optimization/Minify/CSS/Minify.php:15 Stack trace: #0 /home/575368.cloudwaysapps.com/hhchdnqmxq/public_html/wp-content/plugins/elementor/vendor/composer/ClassLoader.php(576): include() #1 /home/575368.cloudwaysapps.com/hhchdnqmxq/public_html/wp-content/plugins/elementor/vendor/composer/ClassLoader.php(427): Composer\Autoload\{closure}() #2 /home/575368.cloudwaysapps.com/hhchdnqmxq/public_html/wp-content/plugins/wp-rocket/inc/Engine/Optimization/Minify/CSS/Subscriber.php(48): Composer\Autoload\ClassLoader->loadClass() #3 /home/575368.cloudwaysapps.com/hhchdnqmxq/public_html/wp-includes/class-wp-hook.php(353): WP_Rocket\Engine\Optimization\Minify\CSS\Subscriber->process() #4 /home/575368.cloudwaysapps.com/hhchdnqmxq/public_html/wp-includes/plugin.php(206): WP_Hook->apply_filters() #5 /home/575368.cloudwaysapps.com/hhchdnqmxq/public_html/wp-content/plugins/wp-rocket/inc/Engine/Optimization/Buffer/Optimization.php(87): apply_filters() #6 [internal function]: WP_Rocket\Engine\Optimization\Buffer\Optimization->maybe_process_buffer() #7 /home/575368.cloudwaysapps.com/hhchdnqmxq/public_html/wp-includes/functions.php(5581): ob_end_flush() #8 /home/575368.cloudwaysapps.com/hhchdnqmxq/public_html/wp-includes/class-wp-hook.php(353): wp_ob_end_flush_all() #9 /home/575368.cloudwaysapps.com/hhchdnqmxq/public_html/wp-includes/class-wp-hook.php(377): WP_Hook->apply_filters() #10 /home/575368.cloudwaysapps.com/hhchdnqmxq/public_html/wp-includes/plugin.php(523): WP_Hook->do_action() #11 /home/575368.cloudwaysapps.com/hhchdnqmxq/public_html/wp-includes/load.php(1308): do_action() #12 [internal function]: shutdown_action_hook() #13 {main} thrown in /home/575368.cloudwaysapps.com/hhchdnqmxq/public_html/wp-content/plugins/wp-rocket/inc/Engine/Optimization/Minify/CSS/Minify.php on line 15