/** * Internal floating toolbar manager. * * Renders schema-declared toolbar chrome against one active editor host * contract so text and non-text hosts share the same floating UI behavior. * * Exposes: SFE.ToolbarManager */ (function() { 'use strict'; window.MWP = window.MWP || {}; window.MWP.SFE = window.MWP.SFE || {}; const SFE = window.MWP.SFE; SFE.ManagerData = SFE.ManagerData || {}; const TOOLBAR_FORMAT_ICONS = { undo: '', redo: '', bold: '', italic: '', strikethrough: '', link: '', alignNone: '', alignWide: '', alignFull: '', alignLeft: '', alignCenter: '', alignRight: '', textAlignLeft: '', textAlignCenter: '', textAlignRight: '', orderedList: '', unorderedList: '', indent: '', outdent: '', textAlignmentDropdown: '', replaceMedia: 'Replace', }; /** * Create one runtime toolbar button definition. * * @param {Object} config Button configuration. * @returns {Object} Runtime toolbar button definition. */ function createToolbarButton(config = {}) { return { icon: config.icon || '', title: config.title || '', action: typeof config.action === 'function' ? config.action : () => {}, className: config.className || '', formatKey: config.formatKey || '', formatType: config.formatType || '', value: Object.prototype.hasOwnProperty.call(config, 'value') ? config.value : undefined, tag: config.tag || '', activeTags: Array.isArray(config.activeTags) ? config.activeTags : [], }; } /** * Create one runtime toolbar dropdown definition. * * @param {Object} config Dropdown configuration. * @returns {Object} Runtime toolbar dropdown definition. */ function createToolbarDropdown(config = {}) { return { type: 'dropdown', title: config.title || '', defaultIcon: config.defaultIcon || '', options: Array.isArray(config.options) ? config.options.filter(Boolean) : [], formatKey: config.formatKey || '', }; } /** * Execute one schema block-attribute operation from a toolbar action. * * @param {Object|null} editor Editor host. * @param {string} operationId Schema operation id. * @param {*} value Requested operation value. * @returns {void} */ function executeSchemaBlockAttributeOperation(editor, operationId, value) { const operationExecutor = SFE.SchemaOperationExecutor || null; if (!operationExecutor || typeof operationExecutor.executeBlockAttributeOperation !== 'function') { return; } operationExecutor.executeBlockAttributeOperation({ editorHost: editor, operationId, value, saveHistory: true, }); } /** * Execute one schema list operation from a toolbar action. * * @param {Object|null} editor Editor host. * @param {string} operationId Schema or primitive operation id. * @returns {void} */ function executeSchemaListOperation(editor, operationId) { if (typeof editor?.executeListStructureOperation !== 'function') { return; } editor.executeListStructureOperation({ kind: operationId, }); } /** * Execute one list type switch from a toolbar action. * * @param {Object|null} editor Editor host. * @param {string} listType List type token. * @returns {void} */ function executeListTypeOperation(editor, listType) { const createTag = listType === 'ordered' ? 'ol' : 'ul'; const oppositeTag = listType === 'ordered' ? 'UL' : 'OL'; const operationExecutor = SFE.SchemaOperationExecutor || null; if (editor?._linkUIActive && typeof editor.closeLinkUI === 'function') { editor.closeLinkUI(); } const selection = window.getSelection(); if (selection?.rangeCount > 0) { const range = selection.getRangeAt(0); let startLi = range.startContainer; let endLi = range.endContainer; while (startLi && startLi !== editor?.element && startLi.tagName !== 'LI') { startLi = startLi.parentNode; } while (endLi && endLi !== editor?.element && endLi.tagName !== 'LI') { endLi = endLi.parentNode; } if (startLi && endLi && startLi !== endLi) { return; } } if ( (editor?.element?.tagName === 'UL' || editor?.element?.tagName === 'OL') && operationExecutor && typeof operationExecutor.executeCurrentListTypeChange === 'function' ) { const result = operationExecutor.executeCurrentListTypeChange({ editorHost: editor, value: listType, }); if (result) { return; } } const listItem = typeof editor?.getCurrentListItem === 'function' ? editor.getCurrentListItem() : null; if (!listItem) { if (typeof editor?.insertListManual === 'function') { editor.insertListManual(createTag); setTimeout(() => { const list = typeof editor?.getParentList === 'function' ? editor.getParentList() : null; if (list) { list.classList.add('wp-block-list'); } }, 10); } return; } const currentList = listItem.parentNode; if ( currentList && currentList.tagName === oppositeTag && typeof editor?.changeListType === 'function' ) { editor.changeListType(currentList, createTag); } } /** * Build one toolbar button for a schema token that toggles inline formatting. * * @param {string} token Token name. * @param {string} title Button title. * @param {string} icon Icon markup. * @param {string} tagName Inline tag name. * @param {string[]} activeTags Active-tag list for toolbar state. * @returns {Object} Runtime toolbar button definition. */ function createInlineFormatButton(token, title, icon, tagName, activeTags) { return createToolbarButton({ formatKey: token, title, icon, activeTags, action: (editor) => { if (typeof editor?.toggleInlineFormat !== 'function') { return; } editor.executeAction(() => { editor.toggleInlineFormat(tagName); }, { saveHistory: false }); }, }); } /** * Build one toolbar button for a schema token that opens link editing. * * @param {string} token Token name. * @param {string} title Button title. * @returns {Object} Runtime toolbar button definition. */ function createLinkButton(token, title) { return createToolbarButton({ formatKey: token, title, icon: TOOLBAR_FORMAT_ICONS.link, action: (editor) => { if (typeof editor?.showLinkUI !== 'function') { return; } const usesElementScopedLinkEditing = typeof editor.supportsElementLinkEditing === 'function' ? editor.supportsElementLinkEditing() : false; const existingLink = typeof editor.getParentElement === 'function' ? editor.getParentElement('a') : null; editor.showLinkUI(usesElementScopedLinkEditing ? null : existingLink); }, }); } /** * Determine whether schema declares element-scoped link editing for the * active text component. * * @param {HTMLElement|null} element Active editable element. * @param {Object} editorOptions Normalized component editor options. * @returns {boolean} True when the component edits its root anchor directly. */ function supportsElementScopedLinkToken(element, editorOptions = {}) { if (!element || element.tagName !== 'A') { return false; } const inlineFormatCapabilities = ( editorOptions?.inlineFormatCapabilities && typeof editorOptions.inlineFormatCapabilities === 'object' ) ? editorOptions.inlineFormatCapabilities : null; const attributeCapabilities = ( editorOptions?.attributeCapabilities && typeof editorOptions.attributeCapabilities === 'object' ) ? editorOptions.attributeCapabilities : null; const buttonLinkCapability = inlineFormatCapabilities?.buttonLink; const buttonLinkTag = typeof buttonLinkCapability?.tag === 'string' ? buttonLinkCapability.tag.trim().toLowerCase() : ''; if (buttonLinkTag !== 'a') { return false; } const attributes = Array.isArray(attributeCapabilities?.buttonLink?.attributes) ? attributeCapabilities.buttonLink.attributes .map(value => (typeof value === 'string' ? value.trim() : '')) .filter(Boolean) : []; return attributes.includes('url'); } /** * Build one toolbar button for a schema list indentation action. * * @param {string} token Token name. * @param {string} title Button title. * @param {string} icon Icon markup. * @param {string} operationId Schema list operation id. * @returns {Object} Runtime toolbar button definition. */ function createListIndentButton(token, title, icon, operationId) { return createToolbarButton({ formatKey: token, title, icon, action: (editor) => { executeSchemaListOperation(editor, operationId); }, }); } /** * Build one toolbar option for schema-backed block alignment. * * @param {string} optionKey Icon/format lookup key. * @param {string} title Option title. * @param {string} value Alignment value. * @returns {Object} Runtime toolbar option definition. */ function createBlockAlignOption(optionKey, title, value) { return createToolbarButton({ formatKey: optionKey, formatType: 'blockAlign', title, icon: TOOLBAR_FORMAT_ICONS[optionKey], value, action: (editor) => { executeSchemaBlockAttributeOperation(editor, 'set_align', value); }, }); } /** * Build one toolbar option for schema-backed text alignment. * * @param {string} optionKey Icon/format lookup key. * @param {string} title Option title. * @param {string} value Alignment value. * @returns {Object} Runtime toolbar option definition. */ function createTextAlignmentOption(optionKey, title, value) { return createToolbarButton({ formatKey: optionKey, formatType: 'textAlignment', title, icon: TOOLBAR_FORMAT_ICONS[optionKey], value, action: (editor) => { executeSchemaBlockAttributeOperation(editor, 'set_text_align', value); }, }); } /** * Build one toolbar option for schema-backed heading level changes. * * @param {string|number} level Heading level or element-tag value. * @returns {Object} Runtime toolbar option definition. */ function createHeadingLevelOption(level) { const value = typeof level === 'string' ? level.trim().toLowerCase() : level; const tag = typeof value === 'number' ? `h${value}` : value; const levelNumber = Number.parseInt(String(tag).replace(/^h/i, ''), 10); const title = tag === 'p' ? 'Paragraph' : tag === 'div' ? 'Div' : `Heading ${levelNumber}`; return createToolbarButton({ formatKey: String(tag), title, icon: title, tag, value, action: (editor) => { executeSchemaBlockAttributeOperation(editor, 'set_heading_level', value); }, }); } /** * Return the canonical built-in block-align toolbar option map. * * @returns {Map} Built-in block-align options keyed by value. */ function getBlockAlignOptionMap() { return new Map([ ['none', createBlockAlignOption('alignNone', 'None', 'none')], ['wide', createBlockAlignOption('alignWide', 'Wide Width', 'wide')], ['full', createBlockAlignOption('alignFull', 'Full Width', 'full')], ['left', createBlockAlignOption('alignLeft', 'Align Left', 'left')], ['center', createBlockAlignOption('alignCenter', 'Align Center', 'center')], ['right', createBlockAlignOption('alignRight', 'Align Right', 'right')], ]); } /** * Create the schema-backed text-alignment dropdown. * * @returns {Object} Runtime toolbar dropdown definition. */ function createTextAlignmentDropdown() { return createToolbarDropdown({ formatKey: 'textAlignment', title: 'Text Alignment', defaultIcon: TOOLBAR_FORMAT_ICONS.textAlignmentDropdown, options: [ createTextAlignmentOption('textAlignLeft', 'Align Text Left', 'left'), createTextAlignmentOption('textAlignCenter', 'Align Text Center', 'center'), createTextAlignmentOption('textAlignRight', 'Align Text Right', 'right'), ], }); } function getSchemaBlockAlignValues(editorOptions = {}) { const operations = Array.isArray(editorOptions?.operations) ? editorOptions.operations : []; const operation = operations.find(candidate => ( String(candidate?.id || '').trim() === 'set_align' && String(candidate?.kind || '').trim() === 'block_attribute_change' )) || null; const operationValues = Array.isArray(operation?.values) ? operation.values : null; const capabilityValues = Array.isArray(editorOptions?.attributeCapabilities?.align?.values) ? editorOptions.attributeCapabilities.align.values : null; const values = operationValues && operationValues.length ? operationValues : capabilityValues; return Array.isArray(values) ? values.map(value => String(value || '').trim().toLowerCase()).filter(Boolean) : []; } function createBlockAlignDropdown(editorOptions = {}) { const allowedValues = getSchemaBlockAlignValues(editorOptions); const optionMap = getBlockAlignOptionMap(); const options = allowedValues.map(value => optionMap.get(value)).filter(Boolean); if (!options.length) { return null; } return createToolbarDropdown({ formatKey: 'align', title: 'Align', defaultIcon: options[0].icon, options }); } /** * Create the schema-backed heading-level dropdown. * * @returns {Object} Runtime toolbar dropdown definition. */ function createHeadingDropdown(editorOptions = {}) { const operation = (editorOptions.operations || []).find(candidate => ( String(candidate?.id || '').trim() === 'set_heading_level' )) || null; const values = Array.isArray(operation?.values) ? operation.values : (editorOptions.attributeCapabilities?.headingLevels?.values || []); return createToolbarDropdown({ formatKey: 'headingLevels', title: 'Heading Level', defaultIcon: 'H', options: values.map(createHeadingLevelOption), }); } /** * Create the shared media-replace toolbar button. * * @returns {Object} Runtime toolbar button definition. */ function createReplaceMediaButton() { return createToolbarButton({ formatKey: 'replaceMedia', icon: TOOLBAR_FORMAT_ICONS.replaceMedia, title: 'Replace Media', className: 'mwp-sfe-editor-btn-text', action: (editor) => { if (typeof editor?.showMediaReplaceUI === 'function') { editor.showMediaReplaceUI(); } } }); } /** * Resolve one built-in schema toolbar token to a runtime format definition. * * @param {string} token Built-in schema toolbar token. * @param {HTMLElement|null} element Active editable element. * @param {Object} editorOptions Normalized editor options. * @returns {Object|null} Runtime toolbar definition or null. */ function resolveSchemaFormatToken(token, element, editorOptions = {}) { const normalizedToken = typeof token === 'string' ? token.trim() : ''; if (!normalizedToken) return null; switch (normalizedToken) { case 'undo': return createToolbarButton({ formatKey: 'undo', title: 'Undo', icon: TOOLBAR_FORMAT_ICONS.undo, action: (editor) => editor?.undo?.(), }); case 'redo': return createToolbarButton({ formatKey: 'redo', title: 'Redo', icon: TOOLBAR_FORMAT_ICONS.redo, action: (editor) => editor?.redo?.(), }); case 'bold': return createInlineFormatButton('bold', 'Bold', TOOLBAR_FORMAT_ICONS.bold, 'strong', ['strong', 'b']); case 'italic': return createInlineFormatButton('italic', 'Italic', TOOLBAR_FORMAT_ICONS.italic, 'em', ['em', 'i']); case 'strikethrough': return createInlineFormatButton('strikethrough', 'Strikethrough', TOOLBAR_FORMAT_ICONS.strikethrough, 's', ['s', 'strike']); case 'link': return createLinkButton('link', 'Link'); case 'buttonLink': return !supportsElementScopedLinkToken(element, editorOptions) ? null : createLinkButton('buttonLink', 'Button Link'); case 'textAlignment': return createTextAlignmentDropdown(); case 'align': return createBlockAlignDropdown(editorOptions); case 'headingLevels': return createHeadingDropdown(editorOptions); case 'orderedList': return createToolbarButton({ formatKey: 'orderedList', title: 'Ordered List', icon: TOOLBAR_FORMAT_ICONS.orderedList, action: (editor) => executeListTypeOperation(editor, 'ordered'), }); case 'unorderedList': return createToolbarButton({ formatKey: 'unorderedList', title: 'Unordered List', icon: TOOLBAR_FORMAT_ICONS.unorderedList, action: (editor) => executeListTypeOperation(editor, 'unordered'), }); case 'indent': return createListIndentButton('indent', 'Indent', TOOLBAR_FORMAT_ICONS.indent, 'indent_list_item'); case 'outdent': return createListIndentButton('outdent', 'Outdent', TOOLBAR_FORMAT_ICONS.outdent, 'outdent_list_item'); case 'replaceMedia': return createReplaceMediaButton(); default: return null; } } /** * Convert one nested schema format token spec into concrete toolbar configs. * * @param {Array} formatsSpec Nested schema token spec. * @param {HTMLElement|null} element Active editable element. * @param {Object} editorOptions Normalized editor options. * @param {number} depth Current recursion depth. * @returns {Array|null} Concrete toolbar config tree. */ function buildFormatsFromSchemaSpec(formatsSpec, element, editorOptions = {}, depth = 0) { if (depth > 3 || !Array.isArray(formatsSpec)) return null; const resolved = []; formatsSpec.forEach(item => { if (typeof item === 'string') { const format = resolveSchemaFormatToken(item, element, editorOptions); if (format) { resolved.push(format); } return; } if (Array.isArray(item)) { const group = buildFormatsFromSchemaSpec(item, element, editorOptions, depth + 1); if (Array.isArray(group) && group.length) { resolved.push(group); } } }); return resolved.length ? resolved : null; } /** * Flatten one nested toolbar definition tree into a single format list. * * @param {Array} items Nested toolbar definition tree. * @returns {Object[]} Flat format list. */ function flattenFormats(items = []) { const flattened = []; (items || []).forEach((item) => { if (Array.isArray(item)) { flattened.push(...flattenFormats(item)); return; } if (!item || typeof item !== 'object') { return; } flattened.push(item); if (item.type === 'dropdown' && Array.isArray(item.options)) { flattened.push(...flattenFormats(item.options)); } }); return flattened; } class ToolbarManager { constructor(host, options = {}) { this.host = host || null; this.options = options || {}; this.toolbar = null; this._toolbarPointerdownHandler = null; this._activeToolbarDropdownWrapper = null; if (this.host && typeof this.host.attachToolbarManager === 'function') { this.host.attachToolbarManager(this); } } getFormats() { return Array.isArray(this.host?.formats) ? this.host.formats : []; } /** * Return one flat runtime toolbar format list. * * @returns {Object[]} Flat toolbar format list. */ getFlatFormats() { return flattenFormats(this.getFormats()); } createToolbar() { if (this.toolbar && this.toolbar.parentNode) this.toolbar.remove(); this.toolbar = document.createElement('div'); this.toolbar.className = 'mwp-sfe-editor-toolbar'; const selectorSvg = ''; const createButton = (format) => { const btn = document.createElement('button'); btn.type = 'button'; btn.className = ['mwp-sfe-editor-btn', format.className || ''].filter(Boolean).join(' '); btn.innerHTML = format.icon; btn.title = format.title; btn.dataset.format = format.title; if (format.title === 'Undo') btn.dataset.action = 'undo'; if (format.title === 'Redo') btn.dataset.action = 'redo'; btn.addEventListener('mousedown', (event) => event.preventDefault()); btn.addEventListener('click', (event) => { event.preventDefault(); format.action(this.host); }); return btn; }; const buildItems = (items, container) => { items.forEach(item => { if (Array.isArray(item)) { const group = document.createElement('div'); group.className = 'mwp-sfe-btn-group'; buildItems(item, group); container.appendChild(group); } else if (item.type === 'dropdown') { const wrapper = document.createElement('div'); wrapper.className = 'mwp-sfe-dropdown'; const toggle = document.createElement('button'); toggle.className = 'mwp-sfe-editor-btn mwp-sfe-dropdown-toggle'; toggle.title = item.title; if (item.formatKey === 'headingLevels') { wrapper.classList.add('mwp-sfe-dropdown-heading-level'); toggle.innerHTML = `${item.defaultIcon} ${selectorSvg}`; } else { toggle.innerHTML = `${item.defaultIcon}`; } const content = document.createElement('div'); content.className = 'mwp-sfe-dropdown-content'; item.options.forEach(opt => { const optBtn = createButton(opt); optBtn.addEventListener('click', () => { if (item.formatKey === 'headingLevels') { toggle.innerHTML = `${opt.icon} ${selectorSvg}`; } else { toggle.innerHTML = `${opt.icon}`; } content.classList.remove('mwp-sfe-show'); }); content.appendChild(optBtn); }); toggle.addEventListener('mousedown', (event) => event.preventDefault()); toggle.addEventListener('click', (event) => { event.preventDefault(); this.toolbar.querySelectorAll('.mwp-sfe-dropdown-content.mwp-sfe-show').forEach(el => { if (el !== content) el.classList.remove('mwp-sfe-show'); }); content.classList.toggle('mwp-sfe-show'); this._activeToolbarDropdownWrapper = content.classList.contains('mwp-sfe-show') ? wrapper : null; }); wrapper.appendChild(toggle); wrapper.appendChild(content); if (container.className !== 'mwp-sfe-btn-group') { const group = document.createElement('div'); group.className = 'mwp-sfe-btn-group'; group.appendChild(wrapper); container.appendChild(group); } else { container.appendChild(wrapper); } } else { const btn = createButton(item); if (container.className !== 'mwp-sfe-btn-group') { const group = document.createElement('div'); group.className = 'mwp-sfe-btn-group'; group.appendChild(btn); container.appendChild(group); } else { container.appendChild(btn); } } }); }; buildItems(this.getFormats(), this.toolbar); this.attachToolbarDropdownCloseHandler(); const toolbarContainer = this.host?.options?.toolbarContainer || null; if (toolbarContainer) { toolbarContainer.innerHTML = ''; toolbarContainer.appendChild(this.toolbar); } else if (this.host?.element?.parentNode) { this.host.element.parentNode.insertBefore(this.toolbar, this.host.element); } this.updateUndoRedoButtons(); } closeToolbarDropdowns() { if (!this.toolbar) { return; } this.toolbar.querySelectorAll('.mwp-sfe-dropdown-content.mwp-sfe-show').forEach((element) => { element.classList.remove('mwp-sfe-show'); }); this._activeToolbarDropdownWrapper = null; } attachToolbarDropdownCloseHandler() { if (!this.toolbar) { return; } if (this._toolbarPointerdownHandler) { document.removeEventListener('pointerdown', this._toolbarPointerdownHandler, true); } this._toolbarPointerdownHandler = (event) => { if ( !this.toolbar || ( this._activeToolbarDropdownWrapper && this._activeToolbarDropdownWrapper.contains(event.target) ) ) { return; } this.closeToolbarDropdowns(); }; document.addEventListener('pointerdown', this._toolbarPointerdownHandler, true); } updateUndoRedoButtons() { if (!this.toolbar) return; const undoBtn = this.toolbar.querySelector('[data-action="undo"]'); const redoBtn = this.toolbar.querySelector('[data-action="redo"]'); const canUndo = typeof this.host?.canUndo === 'function' ? this.host.canUndo() : ( typeof this.host?.historyIndex === 'number' && this.host.historyIndex > 0 ); const canRedo = typeof this.host?.canRedo === 'function' ? this.host.canRedo() : ( typeof this.host?.historyIndex === 'number' && Array.isArray(this.host?.history) && this.host.historyIndex < this.host.history.length - 1 ); if (undoBtn) { undoBtn.disabled = !canUndo; } if (redoBtn) { redoBtn.disabled = !canRedo; } } setToolbarButtonDisabled(title, isDisabled) { if (!this.toolbar || !title) { return; } const button = this.toolbar.querySelector(`button[title="${title}"]`); if (button) { button.disabled = !!isDisabled; } } updateToolbarState() { if (!this.toolbar || !this.host) return; const selection = window.getSelection(); const hasSelectionInEditor = ( selection && selection.rangeCount && typeof this.host.isSelectionInEditor === 'function' && this.host.isSelectionInEditor() ); const parents = []; if (this.host.element?.tagName) { parents.push(this.host.element.tagName.toLowerCase()); } if (hasSelectionInEditor) { const range = selection.getRangeAt(0); let node = range.commonAncestorContainer; while (node && node !== this.host.element) { if (node.nodeType === 1) parents.push(node.tagName.toLowerCase()); node = node.parentNode; } } this.toolbar.querySelectorAll('.mwp-sfe-editor-btn').forEach(btn => { btn.classList.remove('mwp-sfe-editor-btn-active'); }); const checkActive = (format, tags) => { if (!format || !Array.isArray(tags) || !tags.some(tag => parents.includes(tag))) { return false; } const btn = this.toolbar.querySelector(`button[title="${format.title}"]`); if (btn) btn.classList.add('mwp-sfe-editor-btn-active'); return true; }; if (hasSelectionInEditor) { this.getFlatFormats().forEach((format) => { if (!Array.isArray(format?.activeTags) || !format.activeTags.length) { return; } checkActive(format, format.activeTags); }); } const currentAlign = typeof this.host.getBlockAlignState === 'function' ? this.host.getBlockAlignState() : 'none'; const currentTextAlignment = typeof this.host.getTextAlignmentState === 'function' ? this.host.getTextAlignmentState() : 'left'; const currentHeadingLevel = typeof this.host.getHeadingLevelState === 'function' ? this.host.getHeadingLevelState() : null; const getDropdownFormats = (items) => { const dropdowns = []; (items || []).forEach(item => { if (Array.isArray(item)) { dropdowns.push(...getDropdownFormats(item)); return; } if (item && item.type === 'dropdown') { dropdowns.push(item); } }); return dropdowns; }; getDropdownFormats(this.getFormats()).forEach(item => { const toggle = this.toolbar.querySelector(`button[title="${item.title}"]`); if (!toggle) return; const activeOption = item.options.find(opt => { const headingLevel = typeof this.host.getHeadingLevelValueForOption === 'function' ? this.host.getHeadingLevelValueForOption(opt) : null; if ( item.formatKey === 'headingLevels' && headingLevel === currentHeadingLevel ) { return true; } if ( typeof this.host.isTextAlignmentOptionActive === 'function' && this.host.isTextAlignmentOptionActive(opt, currentTextAlignment) ) { return true; } if ( typeof this.host.isBlockAlignOptionActive === 'function' && this.host.isBlockAlignOptionActive(opt, currentAlign) ) { return true; } return false; }); const selectorSvg = ''; if (activeOption) { toggle.innerHTML = item.formatKey === 'headingLevels' ? `${activeOption.icon} ${selectorSvg}` : `${activeOption.icon}`; } else { toggle.innerHTML = item.formatKey === 'headingLevels' ? `${item.defaultIcon} ${selectorSvg}` : `${item.defaultIcon}`; } item.options.forEach(opt => { const optionButton = this.toolbar.querySelector(`button[title="${opt.title}"]`); if (!optionButton) return; let isDisabled = false; if (item.formatKey === 'headingLevels') { const optionLevel = typeof this.host.getHeadingLevelValueForOption === 'function' ? this.host.getHeadingLevelValueForOption(opt) : null; isDisabled = optionLevel === currentHeadingLevel; } else if ( typeof this.host.getTextAlignmentValueForOption === 'function' && this.host.getTextAlignmentValueForOption(opt) ) { isDisabled = typeof this.host.isTextAlignmentOptionActive === 'function' ? this.host.isTextAlignmentOptionActive(opt, currentTextAlignment) : false; } else if ( typeof this.host.getBlockAlignValueForOption === 'function' && this.host.getBlockAlignValueForOption(opt) ) { isDisabled = typeof this.host.isBlockAlignOptionActive === 'function' ? this.host.isBlockAlignOptionActive(opt, currentAlign) : false; } optionButton.disabled = isDisabled; }); }); const operationExecutor = SFE.SchemaOperationExecutor || null; const currentListItem = typeof this.host.getCurrentListItem === 'function' ? this.host.getCurrentListItem() : null; const currentList = ( operationExecutor && typeof operationExecutor.getCurrentListElement === 'function' ) ? operationExecutor.getCurrentListElement(this.host) : ( typeof this.host.getParentList === 'function' ? this.host.getParentList() : null ); const canIndent = typeof this.host.canIndentListItem === 'function' ? this.host.canIndentListItem(currentListItem) : false; const canOutdent = typeof this.host.canOutdentListItem === 'function' ? this.host.canOutdentListItem(currentListItem) : false; this.setToolbarButtonDisabled('Ordered List', !currentList || currentList.tagName === 'OL'); this.setToolbarButtonDisabled('Unordered List', !currentList || currentList.tagName === 'UL'); this.setToolbarButtonDisabled('Indent', !currentListItem || !canIndent); this.setToolbarButtonDisabled('Outdent', !currentListItem || !canOutdent); this.updateUndoRedoButtons(); } destroy(options = {}) { const removeToolbar = options.removeToolbar !== false; if (this._toolbarPointerdownHandler) { document.removeEventListener('pointerdown', this._toolbarPointerdownHandler, true); this._toolbarPointerdownHandler = null; } this._activeToolbarDropdownWrapper = null; if (removeToolbar && this.toolbar && this.toolbar.parentNode) { this.toolbar.remove(); } if (this.host && typeof this.host.detachToolbarManager === 'function') { this.host.detachToolbarManager(this); } this.toolbar = removeToolbar ? null : this.toolbar; this.host = null; } static resolveFormats(editorOptions = {}, element = null) { const schemaFormats = buildFormatsFromSchemaSpec(editorOptions?.formats, element, editorOptions); return Array.isArray(schemaFormats) && schemaFormats.length ? schemaFormats : []; } } SFE.ToolbarManager = ToolbarManager; if (typeof module !== 'undefined' && module.exports) { module.exports = { ToolbarManager }; } })(); ( function( wp ) { if ( ! wp ) { return; } wp.plugins.registerPlugin( 'classic-editor-plugin', { render: function() { var createElement = wp.element.createElement; var PluginMoreMenuItem = wp.editPost.PluginMoreMenuItem; var url = wp.url.addQueryArgs( document.location.href, { 'classic-editor': '', 'classic-editor__forget': '' } ); var linkText = lodash.get( window, [ 'classicEditorPluginL10n', 'linkText' ] ) || 'Switch to classic editor'; return createElement( PluginMoreMenuItem, { icon: 'editor-kitchensink', href: url, }, linkText ); }, } ); } )( window.wp ); Choosing Between Online and Traditional Casinos – M2Doc Technologies LLP

Choosing Between Online and Traditional Casinos

Choosing Between Online and Traditional Casinos

Διαφορετικές εμπειρίες παιχνιδιού

Η επιλογή ανάμεσα σε ένα online καζίνο και ένα παραδοσιακό καζίνο συνδέεται με την εμπειρία που προσφέρει το καθένα. Στα παραδοσιακά καζίνο, οι παίκτες μπορούν να απολαύσουν την ατμόσφαιρα, τη μουσική και την κοινωνική αλληλεπίδραση με άλλους παίκτες. Η φυσική παρουσία σε έναν χώρο τζόγου δημιουργεί μια μοναδική αίσθηση ενθουσιασμού που δύσκολα μπορεί να αναπαραχθεί σε ένα online περιβάλλον. Ωστόσο, οι online πλατφόρμες προσφέρουν ευκολία και πρόσβαση σε παιχνίδια οποιαδήποτε στιγμή, χωρίς να απαιτείται η φυσική παρουσία κάπου. Για παράδειγμα, το lizarocasino-greece.com παρέχει μια ευρεία ποικιλία παιχνιδιών που ικανοποιεί όλες τις προτιμήσεις.

” alt=””>

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

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

Μπόνους και προσφορές

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

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

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

Ασφάλεια και αξιοπιστία

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

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

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

Κοινωνική διάσταση του τζόγου

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

Αντίθετα, στα online καζίνο, η κοινωνική αλληλεπίδραση περιορίζεται κυρίως σε chat rooms και forums. Παρόλο που αυτές οι πλατφόρμες προσφέρουν την ευκαιρία να επικοινωνήσετε με άλλους παίκτες, η αίσθηση της κοινωνικής διάδρασης δεν είναι η ίδια. Ωστόσο, πολλά online καζίνο προσφέρουν live τραπέζια, όπου οι παίκτες μπορούν να αλληλεπιδρούν με dealers και άλλους παίκτες σε πραγματικό χρόνο, προσπαθώντας να φέρουν την ατμόσφαιρα των παραδοσιακών καζίνο στον ψηφιακό κόσμο.

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

Το Lizaro Casino ως ιδανική επιλογή

Το Lizaro Casino προσφέρει μία ολοκληρωμένη εμπειρία για τους παίκτες που αναζητούν την καλύτερη επιλογή μεταξύ online και παραδοσιακού καζίνο. Με πάνω από 10.000 τίτλους παιχνιδιών, οι παίκτες έχουν τη δυνατότητα να επιλέξουν από μια ευρεία γκάμα επιλογών που καλύπτουν όλα τα γούστα. Οι προσφορές μπόνους, συμπεριλαμβανομένου ενός γενναίου μπόνους καλωσορίσματος, κάνουν την επιλογή ακόμα πιο ελκυστική.

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

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

Leave a Comment

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