diff --git a/README.md b/README.md index 7921f64..ae5de30 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,9 @@ The blocks plugins supports a special type of block called `library metadata` wh | searchtags | A comma seperated list of search terms | Allows you to define other terms that could be used when searching for this block in the blocks plugin | false | | tableHeaderBackgroundColor | A hex color (ex #ff3300) | Overrides the table header background color for any blocks in the section or page. | false | | tableHeaderForegroundColor | A hex color (ex #ffffff) | Overrides the table header foreground color for any blocks in the section or page. | false | +| contentEditable | A boolean value (default: true) | Set to false to disable content editing in the preview window. | false | +| disableCopy | A boolean value (default: false) | Set to true to disable the copy button in the preview window. | false | +| hideDetailsView | A boolean value (default: false) | Hide the block details panel inside the preview window. | false | ### Default Library metadata vs Library metadata @@ -170,6 +173,7 @@ The blocks plugin supports the following configuration properties that can be se |----------------|-------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------|----------| | encodeImages | A boolean value that indicates if images should be encoded during copy operations | If your site has a Zero trust network access (ZTNA) service enabled such as Cloudflare Access then images should be encoded for copy/paste operations to work correctly with images. | false | | viewPorts | Full or simplified configuration object, see examples below. | Configuration to overwrite the default viewport sizes. The default is 480px fo mobile, 768px for tablet and 100% of the current window for desktop. | false | +| contentEditable | A boolean value to disable content editing in all block previews. | Set to false to disable content editing. | false | #### Examples diff --git a/src/components/block-list/block-list.js b/src/components/block-list/block-list.js index 64fbe27..73bccfc 100644 --- a/src/components/block-list/block-list.js +++ b/src/components/block-list/block-list.js @@ -310,6 +310,11 @@ export class BlockList extends LitElement { itemName = 'Unnamed Item'; } + // Check if the copy button should be disabled + const disableCopy = sectionLibraryMetadata.disablecopy + ?? defaultLibraryMetadata.disablecopy + ?? false; + // Create the sidenav item for the variant const blockVariantItem = createSideNavItem( itemName, @@ -317,6 +322,7 @@ export class BlockList extends LitElement { false, true, 'sp-icon-copy', + disableCopy, ); // Add search tags to the sidenav item diff --git a/src/components/block-renderer/block-renderer.js b/src/components/block-renderer/block-renderer.js index e1eb2ad..74ab458 100644 --- a/src/components/block-renderer/block-renderer.js +++ b/src/components/block-renderer/block-renderer.js @@ -253,8 +253,16 @@ export class BlockRenderer extends LitElement { * @param {HTMLElement} blockWrapper The wrapped block, includes section metadata * @param {HTMLElement} hostContainer The host container to render the iframe into */ - // eslint-disable-next-line no-unused-vars - async loadBlock(blockName, blockData, blockWrapper, defaultLibraryMetadata, hostContainer) { + + async loadBlock( + blockName, + blockData, + blockWrapper, + defaultLibraryMetadata, + sectionLibraryMetadata, + // eslint-disable-next-line no-unused-vars + hostContainer, + ) { const { context } = AppModel.appStore; const { url: blockURL } = blockData; const origin = blockData.extended @@ -291,8 +299,16 @@ export class BlockRenderer extends LitElement { const sidekickLibraryClass = 'sidekick-library'; content?.classList.add(sidekickLibraryClass); - // Decorate the block with ids - this.decorateEditableElements(content); + // Should the content be editable? + const editable = sectionLibraryMetadata?.contenteditable + ?? defaultLibraryMetadata?.contenteditable + ?? context?.contentEditable + ?? true; + + // Editable can be a boolean or a string + if (editable.toString() === 'true') { + this.decorateEditableElements(content); + } // Clone the block and decorate it const blockClone = blockWrapper.cloneNode(true); diff --git a/src/plugins/blocks/blocks.js b/src/plugins/blocks/blocks.js index c637d53..c67031d 100644 --- a/src/plugins/blocks/blocks.js +++ b/src/plugins/blocks/blocks.js @@ -114,6 +114,7 @@ function renderFrame(contextViewPorts, container) { { }); }); + describe('disable contentEditable', async () => { + it('should be disabled', async () => { + await renderContent('cards', cardsPageUrl, CARDS_DEFAULT_STUB, { contenteditable: false }); + + const iframe = blockRenderer.shadowRoot.querySelector('iframe'); + await waitUntil( + () => recursiveQuery(iframe.contentDocument, '.cards'), + 'Element did not render children', + ); + + const { contentDocument } = iframe; + const cardsBlock = contentDocument.querySelector('.cards'); + cardsBlock.querySelectorAll('li, a, h1, h2, h3, h4, h5, h6').forEach((el) => { + expect(el.getAttribute('contenteditable')).to.be.null; + expect(el.getAttribute('data-library-id')).to.be.null; + }); + + cardsBlock.querySelectorAll('p').forEach((el) => { + expect(el.getAttribute('contenteditable')).to.be.null; + expect(el.getAttribute('data-library-id')).to.be.null; + }); + + cardsBlock.querySelectorAll('strong').forEach((el) => { + expect(el.getAttribute('contenteditable')).to.be.null; + expect(el.getAttribute('data-library-id')).to.be.null; + }); + }); + }); + describe('loadBlock', () => { it('should load a block page', async () => { await renderContent('cards', cardsPageUrl, CARDS_DEFAULT_STUB); diff --git a/test/plugins/blocks/blocks.test.js b/test/plugins/blocks/blocks.test.js index 4b0b926..5950132 100644 --- a/test/plugins/blocks/blocks.test.js +++ b/test/plugins/blocks/blocks.test.js @@ -313,7 +313,7 @@ describe('Blocks Plugin', () => { const mockData = [CARDS_BLOCK_LIBRARY_ITEM, COLUMNS_BLOCK_LIBRARY_ITEM]; mockFetchCardsPlainHTMLSuccess(); mockFetchColumnsPlainHTMLSuccess(); - await decorate(container, mockData); + await decorate(container, mockData, undefined, AppModel.appStore.context); const blockLibrary = container.querySelector('.block-library'); const blocks = blockLibrary.querySelector('sp-split-view .menu .list-container block-list').shadowRoot.querySelectorAll('sp-sidenav-item'); @@ -331,7 +331,7 @@ describe('Blocks Plugin', () => { CARDS_BLOCK_LIBRARY_ITEM, COLUMNS_BLOCK_LIBRARY_ITEM, ]; - await decorate(container, mockData); + await decorate(container, mockData, undefined, AppModel.appStore.context); const blockLibrary = container.querySelector('.block-library'); const blocks = blockLibrary.querySelector('sp-split-view .menu .list-container block-list').shadowRoot.querySelectorAll('sp-sidenav-item'); @@ -345,7 +345,7 @@ describe('Blocks Plugin', () => { const mockData = [NON_EXISTENT_BLOCK_LIBRARY_ITEM]; container.addEventListener(PLUGIN_EVENTS.TOAST, eventSpy); - await decorate(container, mockData); + await decorate(container, mockData, undefined, AppModel.appStore.context); const blockLibrary = container.querySelector('.block-library'); const blocks = blockLibrary.querySelector('sp-split-view .menu .list-container block-list').shadowRoot.querySelectorAll('sp-sidenav-item'); @@ -359,7 +359,7 @@ describe('Blocks Plugin', () => { mockFetchColumnsPlainHTMLSuccess(); const mockData = [CARDS_BLOCK_LIBRARY_ITEM, COLUMNS_BLOCK_LIBRARY_ITEM]; - await decorate(container, mockData); + await decorate(container, mockData, undefined, AppModel.appStore.context); const blockLibrary = container.querySelector('.block-library'); const spSearch = blockLibrary.querySelector('sp-split-view .menu .search sp-search'); @@ -380,7 +380,7 @@ describe('Blocks Plugin', () => { mockFetchColumnsPlainHTMLSuccess(); const mockData = [CARDS_BLOCK_LIBRARY_ITEM, COLUMNS_BLOCK_LIBRARY_ITEM]; - await decorate(container, mockData); + await decorate(container, mockData, undefined, AppModel.appStore.context); const blockLibrary = container.querySelector('.block-library'); const spSearch = blockLibrary.querySelector('sp-split-view .menu .search sp-search'); @@ -402,7 +402,7 @@ describe('Blocks Plugin', () => { const copyBlockSpy = sinon.spy(); const mockData = [CARDS_BLOCK_LIBRARY_ITEM]; - await decorate(container, mockData); + await decorate(container, mockData, undefined, AppModel.appStore.context); const blockLibrary = container.querySelector('.block-library'); const blockList = blockLibrary.querySelector('sp-split-view .menu .list-container block-list'); blockList.addEventListener('CopyBlock', copyBlockSpy); @@ -430,7 +430,7 @@ describe('Blocks Plugin', () => { const copyBlockSpy = sinon.spy(); const mockData = [DEFAULT_CONTENT_LIBRARY_ITEM]; - await decorate(container, mockData); + await decorate(container, mockData, undefined, AppModel.appStore.context); const blockLibrary = container.querySelector('.block-library'); const blockList = blockLibrary.querySelector('sp-split-view .menu .list-container block-list'); blockList.addEventListener('CopyBlock', copyBlockSpy); @@ -922,5 +922,29 @@ describe('Blocks Plugin', () => { expect(frameView.style.width).to.eq('100%'); }); + + it('disable copy button', async () => { + mockFetchCardsPlainHTMLSuccess({ disablecopy: 'true' }); + mockFetchColumnsPlainHTMLSuccess(); + const mockData = [CARDS_BLOCK_LIBRARY_ITEM, COLUMNS_BLOCK_LIBRARY_ITEM]; + + await decorate(container, mockData, undefined, AppModel.appStore.context); + const blockLibrary = container.querySelector('.block-library'); + + const copyButton = blockLibrary.querySelector('sp-split-view .content .details-container .copy-button'); + expect(copyButton.getAttribute('disabled')).to.eq('true'); + }); + + it('hide details view', async () => { + mockFetchCardsPlainHTMLSuccess({ hideDetailsView: 'true' }); + mockFetchColumnsPlainHTMLSuccess(); + const mockData = [CARDS_BLOCK_LIBRARY_ITEM, COLUMNS_BLOCK_LIBRARY_ITEM]; + + await decorate(container, mockData, undefined, AppModel.appStore.context); + const blockLibrary = container.querySelector('.block-library'); + + const splitView = blockLibrary.querySelector('sp-split-view'); + expect(splitView.getAttribute('splitter-pos')).to.eq('-2'); + }); }); });