| 1 |
/** |
| 2 |
* Require alt text before an image can be used. |
| 3 |
* |
| 4 |
* Alt text cannot be demanded while a file is uploading — it is post meta |
| 5 |
* written after the attachment exists, and the field to type it into does not |
| 6 |
* render until the upload has already finished. So the gate is on *use*: the |
| 7 |
* modal's insert button stays disabled while a selected image has no alt text. |
| 8 |
* |
| 9 |
* This is a convenience, not the enforcement. Anything that bypasses the modal — |
| 10 |
* a pasted block, an import, a REST call — bypasses this too, which is why the |
| 11 |
* server also refuses to publish a post containing images without alt text. That |
| 12 |
* check is the one that actually holds. |
| 13 |
*/ |
| 14 |
( function ( wp ) { |
| 15 |
'use strict'; |
| 16 |
|
| 17 |
if ( ! wp || ! wp.media || ! wp.media.view ) { |
| 18 |
return; |
| 19 |
} |
| 20 |
|
| 21 |
var strings = window.aBlocksAltGuard || {}; |
| 22 |
var MESSAGE = strings.message || 'Add alt text before using this image.'; |
| 23 |
var HINT = strings.hint || ''; |
| 24 |
|
| 25 |
/** |
| 26 |
* Does this attachment still need alt text? |
| 27 |
* |
| 28 |
* Only images: audio, video and documents have no alt text to give, and |
| 29 |
* blocking them would be nonsense. |
| 30 |
* |
| 31 |
* @param {Object} model Attachment model. |
| 32 |
* @return {boolean} True when alt text is missing. |
| 33 |
*/ |
| 34 |
function needsAlt( model ) { |
| 35 |
if ( ! model || 'image' !== model.get( 'type' ) ) { |
| 36 |
return false; |
| 37 |
} |
| 38 |
var alt = model.get( 'alt' ); |
| 39 |
return ! alt || ! String( alt ).trim(); |
| 40 |
} |
| 41 |
|
| 42 |
/** |
| 43 |
* Which of the selected attachments are missing alt text. |
| 44 |
* |
| 45 |
* @param {Object} selection Backbone collection. |
| 46 |
* @return {Array} Offending models. |
| 47 |
*/ |
| 48 |
function offenders( selection ) { |
| 49 |
if ( ! selection || ! selection.models ) { |
| 50 |
return []; |
| 51 |
} |
| 52 |
return selection.models.filter( needsAlt ); |
| 53 |
} |
| 54 |
|
| 55 |
/** |
| 56 |
* Enable or disable a toolbar's primary button to match the selection. |
| 57 |
* |
| 58 |
* @param {Object} toolbar Media toolbar view. |
| 59 |
*/ |
| 60 |
function syncToolbar( toolbar ) { |
| 61 |
if ( ! toolbar || ! toolbar.get ) { |
| 62 |
return; |
| 63 |
} |
| 64 |
|
| 65 |
var controller = toolbar.controller; |
| 66 |
var state = controller && controller.state && controller.state(); |
| 67 |
var selection = state && state.get && state.get( 'selection' ); |
| 68 |
var blocked = offenders( selection ); |
| 69 |
|
| 70 |
[ 'select', 'insert' ].forEach( function ( id ) { |
| 71 |
var button = toolbar.get( id ); |
| 72 |
if ( ! button || ! button.$el ) { |
| 73 |
return; |
| 74 |
} |
| 75 |
button.model.set( 'disabled', blocked.length > 0 ); |
| 76 |
button.$el.attr( |
| 77 |
'title', |
| 78 |
blocked.length > 0 ? MESSAGE : '' |
| 79 |
); |
| 80 |
} ); |
| 81 |
|
| 82 |
var $notice = toolbar.$el.find( '.ablocks-alt-required' ); |
| 83 |
if ( blocked.length > 0 ) { |
| 84 |
if ( ! $notice.length ) { |
| 85 |
toolbar.$el.prepend( |
| 86 |
'<div class="ablocks-alt-required" style="flex:1 1 100%;color:#8a1f1f;font-size:12px;padding:4px 0;">' + |
| 87 |
MESSAGE + |
| 88 |
'</div>' |
| 89 |
); |
| 90 |
} |
| 91 |
} else { |
| 92 |
$notice.remove(); |
| 93 |
} |
| 94 |
} |
| 95 |
|
| 96 |
// The toolbar is rebuilt whenever the state changes, so the hook is on the |
| 97 |
// view rather than on a one-time DOM query. |
| 98 |
var Toolbar = wp.media.view.Toolbar; |
| 99 |
wp.media.view.Toolbar = Toolbar.extend( { |
| 100 |
initialize: function () { |
| 101 |
Toolbar.prototype.initialize.apply( this, arguments ); |
| 102 |
|
| 103 |
var self = this; |
| 104 |
var controller = this.controller; |
| 105 |
if ( ! controller || ! controller.state ) { |
| 106 |
return; |
| 107 |
} |
| 108 |
|
| 109 |
var state = controller.state(); |
| 110 |
var selection = state && state.get && state.get( 'selection' ); |
| 111 |
|
| 112 |
if ( selection ) { |
| 113 |
// 'change:alt' fires as soon as the field is edited, so the |
| 114 |
// button unlocks while the user is still looking at it. |
| 115 |
this.listenTo( selection, 'add remove reset change:alt', function () { |
| 116 |
syncToolbar( self ); |
| 117 |
} ); |
| 118 |
} |
| 119 |
|
| 120 |
this.listenTo( controller, 'content:render', function () { |
| 121 |
syncToolbar( self ); |
| 122 |
} ); |
| 123 |
|
| 124 |
window.setTimeout( function () { |
| 125 |
syncToolbar( self ); |
| 126 |
}, 0 ); |
| 127 |
}, |
| 128 |
} ); |
| 129 |
|
| 130 |
// Mark the field itself, so the reason is visible where the fix happens |
| 131 |
// rather than only at the button that refuses. |
| 132 |
if ( wp.media.view.Attachment && wp.media.view.Attachment.Details ) { |
| 133 |
var Details = wp.media.view.Attachment.Details; |
| 134 |
wp.media.view.Attachment.Details = Details.extend( { |
| 135 |
render: function () { |
| 136 |
Details.prototype.render.apply( this, arguments ); |
| 137 |
|
| 138 |
if ( 'image' !== this.model.get( 'type' ) ) { |
| 139 |
return this; |
| 140 |
} |
| 141 |
|
| 142 |
var $alt = this.$el.find( '.setting[data-setting="alt"]' ); |
| 143 |
if ( ! $alt.length ) { |
| 144 |
return this; |
| 145 |
} |
| 146 |
|
| 147 |
$alt.find( 'input' ).attr( 'required', 'required' ); |
| 148 |
|
| 149 |
if ( HINT && ! $alt.next( '.ablocks-alt-hint' ).length ) { |
| 150 |
$alt.after( |
| 151 |
'<span class="ablocks-alt-hint" style="display:block;padding:2px 0 8px;font-size:11px;color:#6c7781;">' + |
| 152 |
HINT + |
| 153 |
'</span>' |
| 154 |
); |
| 155 |
} |
| 156 |
|
| 157 |
return this; |
| 158 |
}, |
| 159 |
} ); |
| 160 |
} |
| 161 |
} )( window.wp ); |
| 162 |
|