├── .coveralls.yml ├── .gitignore ├── .nvmrc ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── bower.json ├── circle.yml ├── dist ├── garnish.js ├── garnish.min.js └── garnish.min.js.map ├── gulpfile.js ├── karma.conf.js ├── karma.coverage.conf.js ├── lib └── Base.js ├── package-lock.json ├── package.json ├── src ├── BaseDrag.js ├── CheckboxSelect.js ├── ContextMenu.js ├── CustomSelect.js ├── DisclosureMenu.js ├── Drag.js ├── DragDrop.js ├── DragMove.js ├── DragSort.js ├── EscManager.js ├── Garnish.js ├── HUD.js ├── MenuBtn.js ├── MixedInput.js ├── Modal.js ├── NiceText.js ├── Select.js ├── SelectMenu.js └── ShortcutManager.js └── test ├── CheckboxSelectTest.js ├── GarnishTest.js ├── HUDTest.js └── MenuTest.js /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: circle-ci -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Source/.idea/ 3 | /.idea 4 | node_modules/* 5 | /coverage 6 | bower_components/* 7 | docs/* -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v11.15.0 -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Garnish Changelog 2 | 3 | ## 0.1.48 4 | 5 | ### Changed 6 | - Modals now remove their shades from the DOM when destroyed. 7 | - HUDs now remove their containers and shades from the DOM when destroyed. 8 | - Improved ESC key handling for menus. 9 | - Context menus and disclosure menus now trigger `show` and `hide` events. 10 | 11 | ## 0.1.47 12 | 13 | ### Added 14 | - Added `Garnish.DisclosureMenu`, for cases where a menu is used to show/hide _content_, as opposed to acting like a form `').appendTo(this.parentInput.$container); 244 | this.$input.css('margin-right', (2 - TextElement.padding) + 'px'); 245 | 246 | this.setWidth(); 247 | 248 | this.addListener(this.$input, 'focus', 'onFocus'); 249 | this.addListener(this.$input, 'blur', 'onBlur'); 250 | this.addListener(this.$input, 'keydown', 'onKeyDown'); 251 | this.addListener(this.$input, 'change', 'checkInput'); 252 | }, 253 | 254 | getIndex: function() { 255 | return this.parentInput.getElementIndex(this.$input); 256 | }, 257 | 258 | buildStage: function() { 259 | this.$stage = $('').appendTo(Garnish.$bod); 260 | 261 | // replicate the textarea's text styles 262 | this.$stage.css({ 263 | position: 'absolute', 264 | top: -9999, 265 | left: -9999, 266 | wordWrap: 'nowrap' 267 | }); 268 | 269 | Garnish.copyTextStyles(this.$input, this.$stage); 270 | }, 271 | 272 | getTextWidth: function(val) { 273 | if (!this.$stage) { 274 | this.buildStage(); 275 | } 276 | 277 | if (val) { 278 | // Ampersand entities 279 | val = val.replace(/&/g, '&'); 280 | 281 | // < and > 282 | val = val.replace(//g, '>'); 284 | 285 | // Spaces 286 | val = val.replace(/ /g, ' '); 287 | } 288 | 289 | this.$stage.html(val); 290 | this.stageWidth = this.$stage.width(); 291 | return this.stageWidth; 292 | }, 293 | 294 | onFocus: function() { 295 | this.focussed = true; 296 | this.interval = setInterval(this.checkInput.bind(this), Garnish.NiceText.interval); 297 | this.checkInput(); 298 | }, 299 | 300 | onBlur: function() { 301 | this.focussed = false; 302 | clearInterval(this.interval); 303 | this.checkInput(); 304 | }, 305 | 306 | onKeyDown: function(ev) { 307 | setTimeout(this.checkInput.bind(this), 1); 308 | 309 | switch (ev.keyCode) { 310 | case Garnish.LEFT_KEY: { 311 | if (this.$input.prop('selectionStart') === 0 && this.$input.prop('selectionEnd') === 0) { 312 | // Set focus to the previous element 313 | this.parentInput.focusPreviousElement(this.$input); 314 | } 315 | break; 316 | } 317 | 318 | case Garnish.RIGHT_KEY: { 319 | if (this.$input.prop('selectionStart') === this.val.length && this.$input.prop('selectionEnd') === this.val.length) { 320 | // Set focus to the next element 321 | this.parentInput.focusNextElement(this.$input); 322 | } 323 | break; 324 | } 325 | 326 | case Garnish.DELETE_KEY: { 327 | if (this.$input.prop('selectionStart') === 0 && this.$input.prop('selectionEnd') === 0) { 328 | // Set focus to the previous element 329 | this.parentInput.focusPreviousElement(this.$input); 330 | ev.preventDefault(); 331 | } 332 | } 333 | } 334 | }, 335 | 336 | getVal: function() { 337 | this.val = this.$input.val(); 338 | return this.val; 339 | }, 340 | 341 | setVal: function(val) { 342 | this.$input.val(val); 343 | this.checkInput(); 344 | }, 345 | 346 | checkInput: function() { 347 | // Has the value changed? 348 | var changed = (this.val !== this.getVal()); 349 | if (changed) { 350 | this.setWidth(); 351 | this.onChange(); 352 | } 353 | 354 | return changed; 355 | }, 356 | 357 | setWidth: function() { 358 | // has the width changed? 359 | if (this.stageWidth !== this.getTextWidth(this.val)) { 360 | // update the textarea width 361 | var width = this.stageWidth + TextElement.padding; 362 | this.$input.width(width); 363 | } 364 | }, 365 | 366 | onChange: $.noop 367 | }, 368 | { 369 | padding: 20 370 | } 371 | ); 372 | -------------------------------------------------------------------------------- /src/Modal.js: -------------------------------------------------------------------------------- 1 | /** global: Garnish */ 2 | /** 3 | * Modal 4 | */ 5 | Garnish.Modal = Garnish.Base.extend( 6 | { 7 | $container: null, 8 | $shade: null, 9 | 10 | visible: false, 11 | 12 | dragger: null, 13 | 14 | desiredWidth: null, 15 | desiredHeight: null, 16 | resizeDragger: null, 17 | resizeStartWidth: null, 18 | resizeStartHeight: null, 19 | 20 | init: function(container, settings) { 21 | // Param mapping 22 | if (typeof settings === 'undefined' && $.isPlainObject(container)) { 23 | // (settings) 24 | settings = container; 25 | container = null; 26 | } 27 | 28 | this.setSettings(settings, Garnish.Modal.defaults); 29 | 30 | // Create the shade 31 | this.$shade = $('
'); 32 | 33 | // If the container is already set, drop the shade below it. 34 | if (container) { 35 | this.$shade.insertBefore(container); 36 | } 37 | else { 38 | this.$shade.appendTo(Garnish.$bod); 39 | } 40 | 41 | if (container) { 42 | this.setContainer(container); 43 | 44 | if (this.settings.autoShow) { 45 | this.show(); 46 | } 47 | } 48 | 49 | Garnish.Modal.instances.push(this); 50 | }, 51 | 52 | setContainer: function(container) { 53 | this.$container = $(container); 54 | 55 | // Is this already a modal? 56 | if (this.$container.data('modal')) { 57 | Garnish.log('Double-instantiating a modal on an element'); 58 | this.$container.data('modal').destroy(); 59 | } 60 | 61 | this.$container.data('modal', this); 62 | 63 | if (this.settings.draggable) { 64 | this.dragger = new Garnish.DragMove(this.$container, { 65 | handle: (this.settings.dragHandleSelector ? this.$container.find(this.settings.dragHandleSelector) : this.$container) 66 | }); 67 | } 68 | 69 | if (this.settings.resizable) { 70 | var $resizeDragHandle = $('
').appendTo(this.$container); 71 | 72 | this.resizeDragger = new Garnish.BaseDrag($resizeDragHandle, { 73 | onDragStart: this._handleResizeStart.bind(this), 74 | onDrag: this._handleResize.bind(this) 75 | }); 76 | } 77 | 78 | this.addListener(this.$container, 'click', function(ev) { 79 | ev.stopPropagation(); 80 | }); 81 | 82 | // Show it if we're late to the party 83 | if (this.visible) { 84 | this.show(); 85 | } 86 | }, 87 | 88 | show: function() { 89 | // Close other modals as needed 90 | if (this.settings.closeOtherModals && Garnish.Modal.visibleModal && Garnish.Modal.visibleModal !== this) { 91 | Garnish.Modal.visibleModal.hide(); 92 | } 93 | 94 | if (this.$container) { 95 | // Move it to the end of so it gets the highest sub-z-index 96 | this.$shade.appendTo(Garnish.$bod); 97 | this.$container.appendTo(Garnish.$bod); 98 | 99 | this.$container.show(); 100 | this.updateSizeAndPosition(); 101 | 102 | this.$shade.velocity('fadeIn', { 103 | duration: 50, 104 | complete: function() { 105 | this.$container.velocity('fadeIn', { 106 | complete: function() { 107 | this.updateSizeAndPosition(); 108 | this.onFadeIn(); 109 | }.bind(this) 110 | }); 111 | }.bind(this) 112 | }); 113 | 114 | if (this.settings.hideOnShadeClick) { 115 | this.addListener(this.$shade, 'click', 'hide'); 116 | } 117 | 118 | this.addListener(Garnish.$win, 'resize', '_handleWindowResize'); 119 | } 120 | 121 | this.enable(); 122 | 123 | if (!this.visible) { 124 | this.visible = true; 125 | Garnish.Modal.visibleModal = this; 126 | 127 | Garnish.shortcutManager.addLayer(); 128 | 129 | if (this.settings.hideOnEsc) { 130 | Garnish.shortcutManager.registerShortcut(Garnish.ESC_KEY, this.hide.bind(this)); 131 | } 132 | 133 | this.trigger('show'); 134 | this.settings.onShow(); 135 | } 136 | }, 137 | 138 | quickShow: function() { 139 | this.show(); 140 | 141 | if (this.$container) { 142 | this.$container.velocity('stop'); 143 | this.$container.show().css('opacity', 1); 144 | 145 | this.$shade.velocity('stop'); 146 | this.$shade.show().css('opacity', 1); 147 | } 148 | }, 149 | 150 | hide: function(ev) { 151 | if (!this.visible) { 152 | return; 153 | } 154 | 155 | this.disable(); 156 | 157 | if (ev) { 158 | ev.stopPropagation(); 159 | } 160 | 161 | if (this.$container) { 162 | this.$container.velocity('fadeOut', {duration: Garnish.FX_DURATION}); 163 | this.$shade.velocity('fadeOut', { 164 | duration: Garnish.FX_DURATION, 165 | complete: this.onFadeOut.bind(this) 166 | }); 167 | 168 | if (this.settings.hideOnShadeClick) { 169 | this.removeListener(this.$shade, 'click'); 170 | } 171 | 172 | this.removeListener(Garnish.$win, 'resize'); 173 | } 174 | 175 | this.visible = false; 176 | Garnish.Modal.visibleModal = null; 177 | Garnish.shortcutManager.removeLayer(); 178 | this.trigger('hide'); 179 | this.settings.onHide(); 180 | }, 181 | 182 | quickHide: function() { 183 | this.hide(); 184 | 185 | if (this.$container) { 186 | this.$container.velocity('stop'); 187 | this.$container.css('opacity', 0).hide(); 188 | 189 | this.$shade.velocity('stop'); 190 | this.$shade.css('opacity', 0).hide(); 191 | } 192 | }, 193 | 194 | updateSizeAndPosition: function() { 195 | if (!this.$container) { 196 | return; 197 | } 198 | 199 | this.$container.css({ 200 | 'width': (this.desiredWidth ? Math.max(this.desiredWidth, 200) : ''), 201 | 'height': (this.desiredHeight ? Math.max(this.desiredHeight, 200) : ''), 202 | 'min-width': '', 203 | 'min-height': '' 204 | }); 205 | 206 | // Set the width first so that the height can adjust for the width 207 | this.updateSizeAndPosition._windowWidth = Garnish.$win.width(); 208 | this.updateSizeAndPosition._width = Math.min(this.getWidth(), this.updateSizeAndPosition._windowWidth - this.settings.minGutter * 2); 209 | 210 | this.$container.css({ 211 | 'width': this.updateSizeAndPosition._width, 212 | 'min-width': this.updateSizeAndPosition._width, 213 | 'left': Math.round((this.updateSizeAndPosition._windowWidth - this.updateSizeAndPosition._width) / 2) 214 | }); 215 | 216 | // Now set the height 217 | this.updateSizeAndPosition._windowHeight = Garnish.$win.height(); 218 | this.updateSizeAndPosition._height = Math.min(this.getHeight(), this.updateSizeAndPosition._windowHeight - this.settings.minGutter * 2); 219 | 220 | this.$container.css({ 221 | 'height': this.updateSizeAndPosition._height, 222 | 'min-height': this.updateSizeAndPosition._height, 223 | 'top': Math.round((this.updateSizeAndPosition._windowHeight - this.updateSizeAndPosition._height) / 2) 224 | }); 225 | 226 | this.trigger('updateSizeAndPosition'); 227 | }, 228 | 229 | onFadeIn: function() { 230 | this.trigger('fadeIn'); 231 | this.settings.onFadeIn(); 232 | }, 233 | 234 | onFadeOut: function() { 235 | this.trigger('fadeOut'); 236 | this.settings.onFadeOut(); 237 | }, 238 | 239 | getHeight: function() { 240 | if (!this.$container) { 241 | throw 'Attempted to get the height of a modal whose container has not been set.'; 242 | } 243 | 244 | if (!this.visible) { 245 | this.$container.show(); 246 | } 247 | 248 | this.getHeight._height = this.$container.outerHeight(); 249 | 250 | if (!this.visible) { 251 | this.$container.hide(); 252 | } 253 | 254 | return this.getHeight._height; 255 | }, 256 | 257 | getWidth: function() { 258 | if (!this.$container) { 259 | throw 'Attempted to get the width of a modal whose container has not been set.'; 260 | } 261 | 262 | if (!this.visible) { 263 | this.$container.show(); 264 | } 265 | 266 | // Chrome might be 1px shy here for some reason 267 | this.getWidth._width = this.$container.outerWidth() + 1; 268 | 269 | if (!this.visible) { 270 | this.$container.hide(); 271 | } 272 | 273 | return this.getWidth._width; 274 | }, 275 | 276 | _handleWindowResize: function(ev) { 277 | // ignore propagated resize events 278 | if (ev.target === window) { 279 | this.updateSizeAndPosition(); 280 | } 281 | }, 282 | 283 | _handleResizeStart: function() { 284 | this.resizeStartWidth = this.getWidth(); 285 | this.resizeStartHeight = this.getHeight(); 286 | }, 287 | 288 | _handleResize: function() { 289 | if (Garnish.ltr) { 290 | this.desiredWidth = this.resizeStartWidth + (this.resizeDragger.mouseDistX * 2); 291 | } 292 | else { 293 | this.desiredWidth = this.resizeStartWidth - (this.resizeDragger.mouseDistX * 2); 294 | } 295 | 296 | this.desiredHeight = this.resizeStartHeight + (this.resizeDragger.mouseDistY * 2); 297 | 298 | this.updateSizeAndPosition(); 299 | }, 300 | 301 | /** 302 | * Destroy 303 | */ 304 | destroy: function() { 305 | if (this.$container) { 306 | this.$container.removeData('modal').remove(); 307 | } 308 | 309 | if (this.$shade) { 310 | this.$shade.remove(); 311 | } 312 | 313 | if (this.dragger) { 314 | this.dragger.destroy(); 315 | } 316 | 317 | if (this.resizeDragger) { 318 | this.resizeDragger.destroy(); 319 | } 320 | 321 | this.base(); 322 | } 323 | }, 324 | { 325 | relativeElemPadding: 8, 326 | defaults: { 327 | autoShow: true, 328 | draggable: false, 329 | dragHandleSelector: null, 330 | resizable: false, 331 | minGutter: 10, 332 | onShow: $.noop, 333 | onHide: $.noop, 334 | onFadeIn: $.noop, 335 | onFadeOut: $.noop, 336 | closeOtherModals: false, 337 | hideOnEsc: true, 338 | hideOnShadeClick: true, 339 | shadeClass: 'modal-shade' 340 | }, 341 | instances: [], 342 | visibleModal: null 343 | } 344 | ); 345 | -------------------------------------------------------------------------------- /src/NiceText.js: -------------------------------------------------------------------------------- 1 | /** global: Garnish */ 2 | /** 3 | * Nice Text 4 | */ 5 | Garnish.NiceText = Garnish.Base.extend( 6 | { 7 | $input: null, 8 | $hint: null, 9 | $stage: null, 10 | $charsLeft: null, 11 | autoHeight: null, 12 | maxLength: null, 13 | showCharsLeft: false, 14 | showingHint: false, 15 | val: null, 16 | inputBoxSizing: 'content-box', 17 | width: null, 18 | height: null, 19 | minHeight: null, 20 | initialized: false, 21 | 22 | init: function(input, settings) { 23 | this.$input = $(input); 24 | this.settings = $.extend({}, Garnish.NiceText.defaults, settings); 25 | 26 | if (this.isVisible()) { 27 | this.initialize(); 28 | } 29 | else { 30 | this.addListener(Garnish.$win, 'resize', 'initializeIfVisible'); 31 | } 32 | }, 33 | 34 | isVisible: function() { 35 | return (this.$input.height() > 0); 36 | }, 37 | 38 | initialize: function() { 39 | if (this.initialized) { 40 | return; 41 | } 42 | 43 | this.initialized = true; 44 | this.removeListener(Garnish.$win, 'resize'); 45 | 46 | this.maxLength = this.$input.attr('maxlength'); 47 | 48 | if (this.maxLength) { 49 | this.maxLength = parseInt(this.maxLength); 50 | } 51 | 52 | if (this.maxLength && (this.settings.showCharsLeft || Garnish.hasAttr(this.$input, 'data-show-chars-left'))) { 53 | this.showCharsLeft = true; 54 | 55 | // Remove the maxlength attribute 56 | this.$input.removeAttr('maxlength'); 57 | } 58 | 59 | // Is this already a transparent text input? 60 | if (this.$input.data('nicetext')) { 61 | Garnish.log('Double-instantiating a transparent text input on an element'); 62 | this.$input.data('nicetext').destroy(); 63 | } 64 | 65 | this.$input.data('nicetext', this); 66 | 67 | this.getVal(); 68 | 69 | this.autoHeight = (this.settings.autoHeight && this.$input.prop('nodeName') === 'TEXTAREA'); 70 | 71 | if (this.autoHeight) { 72 | this.minHeight = this.getHeightForValue(''); 73 | this.updateHeight(); 74 | 75 | // Update height when the window resizes 76 | this.width = this.$input.width(); 77 | this.addListener(Garnish.$win, 'resize', 'updateHeightIfWidthChanged'); 78 | } 79 | 80 | if (this.settings.hint) { 81 | this.$hintContainer = $('
').insertBefore(this.$input); 82 | this.$hint = $('
' + this.settings.hint + '
').appendTo(this.$hintContainer); 83 | this.$hint.css({ 84 | top: (parseInt(this.$input.css('borderTopWidth')) + parseInt(this.$input.css('paddingTop'))), 85 | left: (parseInt(this.$input.css('borderLeftWidth')) + parseInt(this.$input.css('paddingLeft')) + 1) 86 | }); 87 | Garnish.copyTextStyles(this.$input, this.$hint); 88 | 89 | if (this.val) { 90 | this.$hint.hide(); 91 | } 92 | else { 93 | this.showingHint = true; 94 | } 95 | 96 | // Focus the input when clicking on the hint 97 | this.addListener(this.$hint, 'mousedown', function(ev) { 98 | ev.preventDefault(); 99 | this.$input.focus(); 100 | }); 101 | } 102 | 103 | if (this.showCharsLeft) { 104 | this.$charsLeft = $('
').insertAfter(this.$input); 105 | this.updateCharsLeft(); 106 | } 107 | 108 | this.addListener(this.$input, 'textchange', 'onTextChange'); 109 | this.addListener(this.$input, 'keydown', 'onKeyDown'); 110 | }, 111 | 112 | initializeIfVisible: function() { 113 | if (this.isVisible()) { 114 | this.initialize(); 115 | } 116 | }, 117 | 118 | getVal: function() { 119 | this.val = this.$input.val(); 120 | return this.val; 121 | }, 122 | 123 | showHint: function() { 124 | this.$hint.velocity('fadeIn', { 125 | complete: Garnish.NiceText.hintFadeDuration 126 | }); 127 | 128 | this.showingHint = true; 129 | }, 130 | 131 | hideHint: function() { 132 | this.$hint.velocity('fadeOut', { 133 | complete: Garnish.NiceText.hintFadeDuration 134 | }); 135 | 136 | this.showingHint = false; 137 | }, 138 | 139 | onTextChange: function() { 140 | this.getVal(); 141 | 142 | if (this.$hint) { 143 | if (this.showingHint && this.val) { 144 | this.hideHint(); 145 | } 146 | else if (!this.showingHint && !this.val) { 147 | this.showHint(); 148 | } 149 | } 150 | 151 | if (this.autoHeight) { 152 | this.updateHeight(); 153 | } 154 | 155 | if (this.showCharsLeft) { 156 | this.updateCharsLeft(); 157 | } 158 | }, 159 | 160 | onKeyDown: function(ev) { 161 | // If Ctrl/Command + Return is pressed, submit the closest form 162 | if (ev.keyCode === Garnish.RETURN_KEY && Garnish.isCtrlKeyPressed(ev)) { 163 | ev.preventDefault(); 164 | this.$input.closest('form').submit(); 165 | } 166 | }, 167 | 168 | buildStage: function() { 169 | this.$stage = $('').appendTo(Garnish.$bod); 170 | 171 | // replicate the textarea's text styles 172 | this.$stage.css({ 173 | display: 'block', 174 | position: 'absolute', 175 | top: -9999, 176 | left: -9999 177 | }); 178 | 179 | this.inputBoxSizing = this.$input.css('box-sizing'); 180 | 181 | if (this.inputBoxSizing === 'border-box') { 182 | this.$stage.css({ 183 | 'border-top': this.$input.css('border-top'), 184 | 'border-right': this.$input.css('border-right'), 185 | 'border-bottom': this.$input.css('border-bottom'), 186 | 'border-left': this.$input.css('border-left'), 187 | 'padding-top': this.$input.css('padding-top'), 188 | 'padding-right': this.$input.css('padding-right'), 189 | 'padding-bottom': this.$input.css('padding-bottom'), 190 | 'padding-left': this.$input.css('padding-left'), 191 | '-webkit-box-sizing': this.inputBoxSizing, 192 | '-moz-box-sizing': this.inputBoxSizing, 193 | 'box-sizing': this.inputBoxSizing 194 | }); 195 | } 196 | 197 | Garnish.copyTextStyles(this.$input, this.$stage); 198 | }, 199 | 200 | getHeightForValue: function(val) { 201 | if (!this.$stage) { 202 | this.buildStage(); 203 | } 204 | 205 | if (this.inputBoxSizing === 'border-box') { 206 | this.$stage.css('width', this.$input.outerWidth()); 207 | } 208 | else { 209 | this.$stage.css('width', this.$input.width()); 210 | } 211 | 212 | if (!val) { 213 | val = ' '; 214 | for (var i = 1; i < this.$input.prop('rows'); i++) { 215 | val += '
 '; 216 | } 217 | } 218 | else { 219 | // Ampersand entities 220 | val = val.replace(/&/g, '&'); 221 | 222 | // < and > 223 | val = val.replace(//g, '>'); 225 | 226 | // Multiple spaces 227 | val = val.replace(/ {2,}/g, function(spaces) { 228 | // TODO: replace with String.repeat() when more broadly available? 229 | var replace = ''; 230 | for (var i = 0; i < spaces.length - 1; i++) { 231 | replace += ' '; 232 | } 233 | return replace + ' '; 234 | }); 235 | 236 | // Line breaks 237 | val = val.replace(/[\n\r]$/g, '
 '); 238 | val = val.replace(/[\n\r]/g, '
'); 239 | } 240 | 241 | this.$stage.html(val); 242 | 243 | if (this.inputBoxSizing === 'border-box') { 244 | this.getHeightForValue._height = this.$stage.outerHeight(); 245 | } 246 | else { 247 | this.getHeightForValue._height = this.$stage.height(); 248 | } 249 | 250 | if (this.minHeight && this.getHeightForValue._height < this.minHeight) { 251 | this.getHeightForValue._height = this.minHeight; 252 | } 253 | 254 | return this.getHeightForValue._height; 255 | }, 256 | 257 | updateHeight: function() { 258 | // has the height changed? 259 | if (this.height !== (this.height = this.getHeightForValue(this.val))) { 260 | this.$input.css('min-height', this.height); 261 | 262 | if (this.initialized) { 263 | this.onHeightChange(); 264 | } 265 | } 266 | }, 267 | 268 | updateHeightIfWidthChanged: function() { 269 | if (this.isVisible() && this.width !== (this.width = this.$input.width()) && this.width) { 270 | this.updateHeight(); 271 | } 272 | }, 273 | 274 | onHeightChange: function() { 275 | this.settings.onHeightChange(); 276 | }, 277 | 278 | updateCharsLeft: function() { 279 | this.updateCharsLeft._charsLeft = this.maxLength - this.val.length; 280 | this.$charsLeft.html(Garnish.NiceText.charsLeftHtml(this.updateCharsLeft._charsLeft)); 281 | 282 | if (this.updateCharsLeft._charsLeft >= 0) { 283 | this.$charsLeft.removeClass(this.settings.negativeCharsLeftClass); 284 | } 285 | else { 286 | this.$charsLeft.addClass(this.settings.negativeCharsLeftClass); 287 | } 288 | }, 289 | 290 | /** 291 | * Destroy 292 | */ 293 | destroy: function() { 294 | this.$input.removeData('nicetext'); 295 | 296 | if (this.$hint) { 297 | this.$hint.remove(); 298 | } 299 | 300 | if (this.$stage) { 301 | this.$stage.remove(); 302 | } 303 | 304 | this.base(); 305 | } 306 | }, 307 | { 308 | interval: 100, 309 | hintFadeDuration: 50, 310 | charsLeftHtml: function(charsLeft) { 311 | return charsLeft; 312 | }, 313 | defaults: { 314 | autoHeight: true, 315 | showCharsLeft: false, 316 | charsLeftClass: 'chars-left', 317 | negativeCharsLeftClass: 'negative-chars-left', 318 | onHeightChange: $.noop 319 | } 320 | } 321 | ); 322 | -------------------------------------------------------------------------------- /src/SelectMenu.js: -------------------------------------------------------------------------------- 1 | /** global: Garnish */ 2 | /** 3 | * Select Menu 4 | */ 5 | Garnish.SelectMenu = Garnish.CustomSelect.extend( 6 | { 7 | /** 8 | * Constructor 9 | */ 10 | init: function(btn, options, settings, callback) { 11 | // argument mapping 12 | if (typeof settings === 'function') { 13 | // (btn, options, callback) 14 | callback = settings; 15 | settings = {}; 16 | } 17 | 18 | settings = $.extend({}, Garnish.SelectMenu.defaults, settings); 19 | 20 | this.base(btn, options, settings, callback); 21 | 22 | this.selected = -1; 23 | }, 24 | 25 | /** 26 | * Build 27 | */ 28 | build: function() { 29 | this.base(); 30 | 31 | if (this.selected !== -1) { 32 | this._addSelectedOptionClass(this.selected); 33 | } 34 | }, 35 | 36 | /** 37 | * Select 38 | */ 39 | select: function(option) { 40 | // ignore if it's already selected 41 | if (option === this.selected) { 42 | return; 43 | } 44 | 45 | if (this.dom.ul) { 46 | if (this.selected !== -1) { 47 | this.dom.options[this.selected].className = ''; 48 | } 49 | 50 | this._addSelectedOptionClass(option); 51 | } 52 | 53 | this.selected = option; 54 | 55 | // set the button text to the selected option 56 | this.setBtnText($(this.options[option].label).text()); 57 | 58 | this.base(option); 59 | }, 60 | 61 | /** 62 | * Add Selected Option Class 63 | */ 64 | _addSelectedOptionClass: function(option) { 65 | this.dom.options[option].className = 'sel'; 66 | }, 67 | 68 | /** 69 | * Set Button Text 70 | */ 71 | setBtnText: function(text) { 72 | this.dom.$btnLabel.text(text); 73 | } 74 | 75 | }, 76 | { 77 | defaults: { 78 | ulClass: 'menu select' 79 | } 80 | } 81 | ); 82 | -------------------------------------------------------------------------------- /src/ShortcutManager.js: -------------------------------------------------------------------------------- 1 | /** global: Garnish */ 2 | /** 3 | * Keyboard shortcut manager class 4 | * 5 | * This can be used to map keyboard events to the current UI "layer" (whether that's the base document, 6 | * a modal, an HUD, or a menu). 7 | */ 8 | Garnish.ShortcutManager = Garnish.Base.extend( 9 | { 10 | shortcuts: null, 11 | layer: 0, 12 | 13 | init: function() { 14 | this.shortcuts = [[]]; 15 | this.addListener(Garnish.$bod, 'keydown', 'triggerShortcut'); 16 | }, 17 | 18 | addLayer: function() { 19 | this.layer++; 20 | this.shortcuts.push([]); 21 | return this; 22 | }, 23 | 24 | removeLayer: function() { 25 | if (this.layer === 0) { 26 | throw 'Can’t remove the base layer.'; 27 | } 28 | this.layer--; 29 | this.shortcuts.pop(); 30 | return this; 31 | }, 32 | 33 | registerShortcut: function(shortcut, callback, layer) { 34 | shortcut = this._normalizeShortcut(shortcut); 35 | if (typeof layer === 'undefined') { 36 | layer = this.layer; 37 | } 38 | this.shortcuts[layer].push({ 39 | key: JSON.stringify(shortcut), 40 | shortcut: shortcut, 41 | callback: callback, 42 | }); 43 | return this; 44 | }, 45 | 46 | unregisterShortcut: function(shortcut, layer) { 47 | shortcut = this._normalizeShortcut(shortcut); 48 | var key = JSON.stringify(shortcut); 49 | if (typeof layer === 'undefined') { 50 | layer = this.layer; 51 | } 52 | for (var i = 0; i < this.shortcuts[layer].length; i++) { 53 | if (this.shortcuts[layer][i].key === key) { 54 | this.shortcuts[layer].splice(i, 1); 55 | break; 56 | } 57 | } 58 | return this; 59 | }, 60 | 61 | _normalizeShortcut: function(shortcut) { 62 | if (typeof shortcut === 'number') { 63 | shortcut = {keyCode: shortcut}; 64 | } 65 | 66 | if (typeof shortcut.keyCode !== 'number') { 67 | throw 'Invalid shortcut'; 68 | } 69 | 70 | return { 71 | keyCode: shortcut.keyCode, 72 | ctrl: !!shortcut.ctrl, 73 | shift: !!shortcut.shift, 74 | alt: !!shortcut.alt, 75 | }; 76 | }, 77 | 78 | triggerShortcut: function(ev) { 79 | var shortcut; 80 | for (var i = 0; i < this.shortcuts[this.layer].length; i++) { 81 | shortcut = this.shortcuts[this.layer][i].shortcut; 82 | if ( 83 | shortcut.keyCode === ev.keyCode && 84 | shortcut.ctrl === Garnish.isCtrlKeyPressed(ev) && 85 | shortcut.shift === ev.shiftKey && 86 | shortcut.alt === ev.altKey 87 | ) { 88 | ev.preventDefault(); 89 | this.shortcuts[this.layer][i].callback(ev); 90 | break; 91 | } 92 | } 93 | }, 94 | } 95 | ); 96 | 97 | Garnish.shortcutManager = new Garnish.ShortcutManager(); 98 | -------------------------------------------------------------------------------- /test/CheckboxSelectTest.js: -------------------------------------------------------------------------------- 1 | describe("Garnish.CheckboxSelect tests", function() { 2 | 3 | var $container = $('
'); 4 | $divAll = $('
').appendTo($container); 5 | $all = $('').appendTo($divAll); 6 | $divOption1 = $('
').appendTo($container); 7 | $option1 = $('').appendTo($divOption1); 8 | $divOption2 = $('
').appendTo($container); 9 | $option2 = $('').appendTo($divOption2); 10 | 11 | var checkboxSelect = new Garnish.CheckboxSelect($container); 12 | 13 | it("$all should be defined", function() { 14 | expect(checkboxSelect.$all.get(0)).toBeDefined(); 15 | }); 16 | 17 | it("$options length should be greater than 0", function() { 18 | expect(checkboxSelect.$options.length).toBeGreaterThan(0); 19 | }); 20 | 21 | it("all options should be checked", function() { 22 | checkboxSelect.$all.prop('checked', true); 23 | checkboxSelect.$all.trigger('change'); 24 | 25 | var $option = $(checkboxSelect.$options[0]); 26 | 27 | expect($option.prop('checked')).toBe(true); 28 | }); 29 | 30 | it("Instantiating the checkbox select a second time should destroy the first instance and create a new one", function() { 31 | 32 | var checkboxSelect2 = new Garnish.CheckboxSelect($container); 33 | 34 | expect(checkboxSelect._namespace).not.toEqual(checkboxSelect2._namespace); 35 | }); 36 | 37 | }); -------------------------------------------------------------------------------- /test/GarnishTest.js: -------------------------------------------------------------------------------- 1 | describe("Garnish tests", function() { 2 | 3 | it("Checks whether a variable is an array.", function() { 4 | 5 | var mockArray = ['row 1', 'row 2']; 6 | 7 | expect(Garnish.isArray(mockArray)).toBe(true); 8 | }); 9 | 10 | it("Checks whether a variable is a string.", function() { 11 | 12 | var mockString = "Dummy string"; 13 | 14 | expect(Garnish.isString(mockString)).toBe(true); 15 | }); 16 | 17 | it("Checks whether an element has an attribute.", function() { 18 | 19 | var $element = $('
'); 20 | 21 | expect(Garnish.hasAttr($element, 'class')).toBe(true); 22 | }); 23 | 24 | }); -------------------------------------------------------------------------------- /test/HUDTest.js: -------------------------------------------------------------------------------- 1 | describe("Garnish.HUD tests", function() { 2 | 3 | var $trigger = $('Trigger').appendTo(Garnish.$bod); 4 | var bodyContents = 'test'; 5 | 6 | var hud = new Garnish.HUD($trigger, bodyContents); 7 | 8 | it("Should instantiate the HUD.", function() { 9 | 10 | hudInstantiated = false; 11 | 12 | if(hud) 13 | { 14 | hudInstantiated = true; 15 | } 16 | 17 | expect(hudInstantiated).toBe(true); 18 | }); 19 | 20 | }); -------------------------------------------------------------------------------- /test/MenuTest.js: -------------------------------------------------------------------------------- 1 | describe("Garnish.Menu tests", function() { 2 | 3 | var $menu = $('