From c80e6a602920b81f2d7b107ed5f90d2d9e4d5b10 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 17 Feb 2017 16:20:31 +0100 Subject: [PATCH 1/3] Adding keyboard shortcuts to navigate on the Block Insert Menu --- blocks.js | 179 ++++++++++++++++++++++++++++++++++++++++++++++++----- index.html | 2 +- style.css | 5 ++ 3 files changed, 170 insertions(+), 16 deletions(-) diff --git a/blocks.js b/blocks.js index 8873400246e0d..d5b3ee9b51a6d 100644 --- a/blocks.js +++ b/blocks.js @@ -45,44 +45,52 @@ var config = { ], blocks: [ { + id: 'paragraph', label: 'Paragraph', icon: '', - categories: [ 'frequent' ] + category: 'frequent' }, { + id: 'heading', label: 'Heading', icon: 'Heading', - categories: [ 'frequent' ] + category: 'frequent' }, { + id: 'image', label: 'Image', icon: 'Image', - categories: [ 'frequent' ] + category: 'frequent' }, { + id: 'quote', label: 'Quote', icon: '', - categories: [ 'frequent' ] + category: 'frequent' }, { + id: 'gallery', label: 'Gallery', icon: '', - categories: [ 'media' ] + category: 'media' }, { + id: 'unordered-list', label: 'Unordered List', icon: '', - categories: [ 'frequent' ] + category: 'frequent' }, { + id: 'ordered-list', label: 'Ordered List', icon: '', - categories: [ 'frequent' ] + category: 'frequent' }, { + id: 'embed', label: 'Embed', icon: '', - categories: [ 'media' ] + category: 'media' } ] }; @@ -107,6 +115,16 @@ var imageAlignRight = queryFirst( '.block-image__align-right' ); var selectedBlock = null; var searchBlockFilter = ''; +var blockMenuOpened = false; +var menuSelectedBlock = null; + +var orderedBlocks = config.blockCategories.reduce( function( memo, category ) { + var categoryBlocks = config.blocks.filter( function( block ) { + return block.category === category.id; + } ); + + return memo.concat( categoryBlocks ); +}, [] ); var supportedBlockTags = Object.keys( config.tagTypes ) .slice( 0, -1 ) // remove 'default' option @@ -128,6 +146,7 @@ attachBlockHandlers(); attachControlActions(); attachTypeSwitcherActions(); attachBlockMenuSearch(); +attachKeyboardShortcuts(); /** * Core logic @@ -300,7 +319,7 @@ function attachTypeSwitcherActions() { } ); } -function fillBlockMenu() { +function renderBlockMenu() { insertBlockMenuContent.innerHTML = ''; config.blockCategories.forEach( function ( category ) { var node = document.createElement( 'div' ); @@ -314,13 +333,13 @@ function fillBlockMenu() { node.appendChild( nodeBlocks ); var categoryBlocks = config.blocks .filter( function( block ) { - return block.categories.indexOf( category.id ) !== -1 + return block.category === category.id && block.label.toLowerCase().indexOf( searchBlockFilter.toLowerCase() ) !== -1; } ); categoryBlocks .forEach( function( block ) { var node = document.createElement( 'div' ); - node.className = 'insert-block__block'; + node.className = 'insert-block__block block-' + block.id + ( menuSelectedBlock === block ? ' is-active' : '' ); node.innerHTML = block.icon + ' ' + block.label; nodeBlocks.appendChild(node); } ); @@ -339,11 +358,134 @@ function fillBlockMenu() { function attachBlockMenuSearch() { insertBlockMenuSearchInput.addEventListener( 'keyup', filterBlockMenu, false ); insertBlockMenuSearchInput.addEventListener( 'input', filterBlockMenu, false ); - fillBlockMenu(); + selectBlockInMenu( 'none' ); + renderBlockMenu(); function filterBlockMenu( event ) { searchBlockFilter = event.target.value; - fillBlockMenu(); + selectBlockInMenu( 'none' ); + renderBlockMenu(); + } +} + +/** + * Select a block in the block menu + * @param direction direction from the current position (up/down/left/right) + */ +function selectBlockInMenu( direction ) { + var filteredBlocks = orderedBlocks.filter( function( block ) { + return block.label.toLowerCase().indexOf( searchBlockFilter.toLowerCase() ) !== -1; + } ); + var countBlocksByCategories = filteredBlocks.reduce( function( memo, block ) { + if ( ! memo[ block.category ] ) { + memo[ block.category ] = 0; + } + memo[ block.category ]++; + return memo; + }, {} ); + + var selectedBlockIndex = filteredBlocks.indexOf( menuSelectedBlock ); + selectedBlockIndex = selectedBlockIndex === -1 ? 0 : selectedBlockIndex; + var currentBlock = filteredBlocks[ selectedBlockIndex ]; + var previousBlock = filteredBlocks[ selectedBlockIndex - 1 ]; + var nextBlock = filteredBlocks[ selectedBlockIndex + 1 ]; + var offset = 0; + switch ( direction ) { + case 'up': + offset = ( + currentBlock + && filteredBlocks[ selectedBlockIndex - 2 ] + && ( + filteredBlocks[ selectedBlockIndex - 2 ].category === currentBlock.category + || countBlocksByCategories[ previousBlock.category ] % 2 === 0 + ) + ) ? -2 : -1; + break; + case 'down': + offset = ( + currentBlock + && filteredBlocks[ selectedBlockIndex + 2 ] + && ( + currentBlock.category === filteredBlocks[ selectedBlockIndex + 2 ].category + || filteredBlocks[ selectedBlockIndex + 2 ].category === nextBlock.category + || nextBlock.category === currentBlock.category + ) + ) ? 2 : 1; + break; + case 'right': + offset = 1; + break; + case 'left': + offset = -1; + break; + } + + menuSelectedBlock = filteredBlocks[ selectedBlockIndex + offset ] || menuSelectedBlock; + + // Hack to wait for the rerender before scrolling + setTimeout( function() { + var blockElement = queryFirst( '.insert-block__block.block-' + menuSelectedBlock.id ); + if ( blockElement ) { + insertBlockMenuContent.scrollTop = blockElement.offsetTop - 23; + } + } ); +} + +function attachKeyboardShortcuts() { + document.addEventListener( 'keypress', handleKeyPress, false ); + document.addEventListener( 'keyup', handleKeyUp, false ); + + function handleKeyPress( event ) { + if ( + /[a-zA-Z0-9-_ ]/.test( String.fromCharCode( event.keyCode ) ) + && document.activeElement === insertBlockMenuContent + ) { + searchBlockFilter = searchBlockFilter + String.fromCharCode( event.keyCode ); + insertBlockMenuSearchInput.value = searchBlockFilter; + selectBlockInMenu( '' ); + renderBlockMenu(); + } + } + + function handleKeyUp( event ) { + switch ( event.keyCode ) { + case 191: + if ( document.activeElement === editor ) return; + event.preventDefault(); + ! blockMenuOpened && openBlockMenu(); + break; + case 13: + event.preventDefault(); + blockMenuOpened && hideMenu(); + break; + case 37: + event.preventDefault(); + selectBlockInMenu( 'left' ); + renderBlockMenu(); + break; + case 38: + event.preventDefault(); + selectBlockInMenu( 'up' ); + renderBlockMenu(); + break; + case 39: + event.preventDefault(); + selectBlockInMenu( 'right' ); + renderBlockMenu(); + break; + case 40: + event.preventDefault(); + selectBlockInMenu( 'down' ); + renderBlockMenu(); + break; + case 8: + event.preventDefault(); + searchBlockFilter = searchBlockFilter.substr( 0, searchBlockFilter.length - 1 ); + insertBlockMenuSearchInput.value = searchBlockFilter; + selectBlockInMenu( '' ); + renderBlockMenu(); + break; + } } } @@ -394,13 +536,20 @@ function siblingGetter( direction ) { function openBlockMenu( event ) { hideInlineControls(); clearBlocks(); - event.stopPropagation(); + event && event.stopPropagation(); insertBlockMenu.style.display = 'block'; - insertBlockMenuSearchInput.focus(); + blockMenuOpened = true; + searchBlockFilter = ''; + insertBlockMenuSearchInput.value = ''; + menuSelectedBlock = false; + insertBlockMenuContent.focus(); + selectBlockInMenu( 'none' ); + renderBlockMenu(); } function hideMenu() { insertBlockMenu.style.display = 'none'; + blockMenuOpened = false; } function showSwitcherMenu( event ) { diff --git a/index.html b/index.html index 4510c93e07958..a0f1892c4fd01 100644 --- a/index.html +++ b/index.html @@ -84,7 +84,7 @@

1.0 Is The Loneliest Number

-
+
diff --git a/style.css b/style.css index 54aab47861bbb..ba214c7618209 100644 --- a/style.css +++ b/style.css @@ -428,6 +428,10 @@ img.is-selected { overflow: auto; } +.insert-block__content:focus { + outline: none; +} + .insert_block__category-blocks { display: flex; flex-flow: row wrap; @@ -442,6 +446,7 @@ img.is-selected { align-items: center; cursor: pointer; } +.insert-block__block.is-active, .insert-block__block:hover, .switch-block__block:hover { background: #f0f2f4; From 78c0334a4fae55ab14635679a153ebe6b6869a63 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Mon, 20 Feb 2017 10:34:01 +0100 Subject: [PATCH 2/3] Changes per review --- blocks.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/blocks.js b/blocks.js index d5b3ee9b51a6d..7bdbedba39853 100644 --- a/blocks.js +++ b/blocks.js @@ -425,7 +425,12 @@ function selectBlockInMenu( direction ) { // Hack to wait for the rerender before scrolling setTimeout( function() { var blockElement = queryFirst( '.insert-block__block.block-' + menuSelectedBlock.id ); - if ( blockElement ) { + if ( + blockElement && ( + blockElement.offsetTop + blockElement.offsetHeight > insertBlockMenuContent.clientHeight + insertBlockMenuContent.scrollTop + || blockElement.offsetTop < insertBlockMenuContent.scrollTop + ) + ) { insertBlockMenuContent.scrollTop = blockElement.offsetTop - 23; } } ); @@ -433,7 +438,7 @@ function selectBlockInMenu( direction ) { function attachKeyboardShortcuts() { document.addEventListener( 'keypress', handleKeyPress, false ); - document.addEventListener( 'keyup', handleKeyUp, false ); + document.addEventListener( 'keydown', handleKeyDown, false ); function handleKeyPress( event ) { if ( @@ -444,16 +449,15 @@ function attachKeyboardShortcuts() { insertBlockMenuSearchInput.value = searchBlockFilter; selectBlockInMenu( '' ); renderBlockMenu(); + } else if ( '/' === String.fromCharCode( event.keyCode ) ) { + if ( document.activeElement === editor ) return; + event.preventDefault(); + ! blockMenuOpened && openBlockMenu(); } } - function handleKeyUp( event ) { + function handleKeyDown( event ) { switch ( event.keyCode ) { - case 191: - if ( document.activeElement === editor ) return; - event.preventDefault(); - ! blockMenuOpened && openBlockMenu(); - break; case 13: event.preventDefault(); blockMenuOpened && hideMenu(); From 88feb95fb78ed7f94bc9f0cf1dc1488a3582114d Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Mon, 20 Feb 2017 13:17:55 +0100 Subject: [PATCH 3/3] Changes per review --- blocks.js | 102 +++++++++++++++++++++++++++++------------------------ index.html | 2 +- 2 files changed, 57 insertions(+), 47 deletions(-) diff --git a/blocks.js b/blocks.js index 7bdbedba39853..230305d412012 100644 --- a/blocks.js +++ b/blocks.js @@ -113,11 +113,23 @@ var imageAlignNone = queryFirst( '.block-image__no-align' ); var imageAlignLeft = queryFirst( '.block-image__align-left' ); var imageAlignRight = queryFirst( '.block-image__align-right' ); +// Contants +var KEY_ENTER = 13; +var KEY_ARROW_LEFT = 37; +var KEY_ARROW_UP = 38; +var KEY_ARROW_RIGHT = 39; +var KEY_ARROW_DOWN = 40; + +// Editor Variables var selectedBlock = null; + +// Block Menu Variables +var previouslyFocusedBlock = null; var searchBlockFilter = ''; var blockMenuOpened = false; var menuSelectedBlock = null; +// Helper variables var orderedBlocks = config.blockCategories.reduce( function( memo, category ) { var categoryBlocks = config.blocks.filter( function( block ) { return block.category === category.id; @@ -163,6 +175,14 @@ function getBlocks() { supportedBlockTags.map( query ) ); } +function getFocusedBlock() { + var focusedBlocks = getBlocks().filter( function( block ) { + return block.contains( window.getSelection().anchorNode ); + } ); + + return focusedBlocks.length ? focusedBlocks[ 0 ] : null; +} + function selectBlock( event ) { clearBlocks(); event.stopPropagation(); @@ -358,12 +378,12 @@ function renderBlockMenu() { function attachBlockMenuSearch() { insertBlockMenuSearchInput.addEventListener( 'keyup', filterBlockMenu, false ); insertBlockMenuSearchInput.addEventListener( 'input', filterBlockMenu, false ); - selectBlockInMenu( 'none' ); + selectBlockInMenu(); renderBlockMenu(); function filterBlockMenu( event ) { searchBlockFilter = event.target.value; - selectBlockInMenu( 'none' ); + selectBlockInMenu(); renderBlockMenu(); } } @@ -391,7 +411,7 @@ function selectBlockInMenu( direction ) { var nextBlock = filteredBlocks[ selectedBlockIndex + 1 ]; var offset = 0; switch ( direction ) { - case 'up': + case KEY_ARROW_UP: offset = ( currentBlock && filteredBlocks[ selectedBlockIndex - 2 ] @@ -401,7 +421,7 @@ function selectBlockInMenu( direction ) { ) ) ? -2 : -1; break; - case 'down': + case KEY_ARROW_DOWN: offset = ( currentBlock && filteredBlocks[ selectedBlockIndex + 2 ] @@ -412,10 +432,10 @@ function selectBlockInMenu( direction ) { ) ) ? 2 : 1; break; - case 'right': + case KEY_ARROW_RIGHT: offset = 1; break; - case 'left': + case KEY_ARROW_LEFT: offset = -1; break; } @@ -441,52 +461,28 @@ function attachKeyboardShortcuts() { document.addEventListener( 'keydown', handleKeyDown, false ); function handleKeyPress( event ) { - if ( - /[a-zA-Z0-9-_ ]/.test( String.fromCharCode( event.keyCode ) ) - && document.activeElement === insertBlockMenuContent - ) { - searchBlockFilter = searchBlockFilter + String.fromCharCode( event.keyCode ); - insertBlockMenuSearchInput.value = searchBlockFilter; - selectBlockInMenu( '' ); - renderBlockMenu(); - } else if ( '/' === String.fromCharCode( event.keyCode ) ) { - if ( document.activeElement === editor ) return; - event.preventDefault(); - ! blockMenuOpened && openBlockMenu(); + if ( '/' === String.fromCharCode( event.keyCode ) && ! blockMenuOpened ) { + var focusedBlock = getFocusedBlock(); + if ( document.activeElement !== editor || ( focusedBlock && ! focusedBlock.textContent ) ) { + event.preventDefault(); + openBlockMenu(); + } } } function handleKeyDown( event ) { + if ( ! blockMenuOpened ) return; switch ( event.keyCode ) { - case 13: - event.preventDefault(); - blockMenuOpened && hideMenu(); - break; - case 37: - event.preventDefault(); - selectBlockInMenu( 'left' ); - renderBlockMenu(); - break; - case 38: - event.preventDefault(); - selectBlockInMenu( 'up' ); - renderBlockMenu(); - break; - case 39: + case KEY_ENTER: event.preventDefault(); - selectBlockInMenu( 'right' ); - renderBlockMenu(); + hideMenu(); break; - case 40: + case KEY_ARROW_DOWN: + case KEY_ARROW_UP: + case KEY_ARROW_LEFT: + case KEY_ARROW_RIGHT: event.preventDefault(); - selectBlockInMenu( 'down' ); - renderBlockMenu(); - break; - case 8: - event.preventDefault(); - searchBlockFilter = searchBlockFilter.substr( 0, searchBlockFilter.length - 1 ); - insertBlockMenuSearchInput.value = searchBlockFilter; - selectBlockInMenu( '' ); + selectBlockInMenu( event.keyCode ); renderBlockMenu(); break; } @@ -546,14 +542,19 @@ function openBlockMenu( event ) { searchBlockFilter = ''; insertBlockMenuSearchInput.value = ''; menuSelectedBlock = false; - insertBlockMenuContent.focus(); - selectBlockInMenu( 'none' ); + previouslyFocusedBlock = getFocusedBlock(); + insertBlockMenuSearchInput.focus(); + selectBlockInMenu(); renderBlockMenu(); } function hideMenu() { + if ( ! blockMenuOpened ) return; insertBlockMenu.style.display = 'none'; blockMenuOpened = false; + if ( previouslyFocusedBlock ) { + setCaret( previouslyFocusedBlock ); + } } function showSwitcherMenu( event ) { @@ -591,6 +592,15 @@ function setElementState( className, event ) { } } +function setCaret( element ) { + var range = document.createRange(); + range.setStart( element.childNodes[0] ,0 ); + range.collapse( true ); + var selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange( range ); +} + function l( data ) { console.log.apply( console.log, arguments ); return data; diff --git a/index.html b/index.html index a0f1892c4fd01..4510c93e07958 100644 --- a/index.html +++ b/index.html @@ -84,7 +84,7 @@

1.0 Is The Loneliest Number

-
+