From 170f1ddb964cdea2a7c0e6b4caa9f5e4b03fa736 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?"J.=20Rene=CC=81e=20Beach"?= Date: Wed, 2 Jan 2013 16:27:49 -0500 Subject: [PATCH] Issue #1741498 by jessebeach: Add a mobile preview bar to Drupal core MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit 16205157cc4a6bc685b780eab2f3930eb135d2b2 Author: J. Renée Beach Date: Wed Jan 2 16:25:59 2013 -0500 Added a slight animation for typed-in values. Signed-off-by: J. Renée Beach commit 087004b22c9c0023eee6cb0a748170567fb9f2b1 Author: J. Renée Beach Date: Wed Jan 2 16:12:05 2013 -0500 Dimensions can now be changed with the input box. Signed-off-by: J. Renée Beach commit 129ca113ff9f7133916ac9dcedf51c668a2fb858 Author: J. Renée Beach Date: Wed Jan 2 15:57:11 2013 -0500 Created an updateDimensions function and moved the manipulation of the dimension variables in the closure to it. Signed-off-by: J. Renée Beach commit a04b4e4f6d1414cc64f8cd524a3ee77b28a0b937 Author: J. Renée Beach Date: Wed Jan 2 15:33:40 2013 -0500 Moved the frame and slider positioning refresh into its own function. Signed-off-by: J. Renée Beach commit 38cca4cbf26462fa0e06cf0be8b5b47404606c38 Author: J. Renée Beach Date: Wed Jan 2 15:06:18 2013 -0500 Correctly processing text in the size input now. Signed-off-by: J. Renée Beach commit b3a392dadd2f5f80dc5e4097c25e622b6ab81fb0 Author: J. Renée Beach Date: Wed Jan 2 14:50:40 2013 -0500 Input validation for digits on the size input. Signed-off-by: J. Renée Beach commit 363e46280c666d53245c57ac4c561559d40a7d65 Author: J. Renée Beach Date: Wed Jan 2 14:50:18 2013 -0500 patch 2 Signed-off-by: J. Renée Beach Signed-off-by: J. Renée Beach --- core/modules/layout/css/layout.base.css | 65 ++++++ core/modules/layout/css/layout.theme.css | 23 ++ core/modules/layout/images/handle.png | 4 + core/modules/layout/images/ruler.png | 7 + core/modules/layout/js/layout.js | 355 ++++++++++++++++++++++++++++++ core/modules/layout/layout.module | 63 ++++++ 6 files changed, 517 insertions(+) create mode 100644 core/modules/layout/css/layout.base.css create mode 100644 core/modules/layout/css/layout.theme.css create mode 100644 core/modules/layout/images/handle.png create mode 100644 core/modules/layout/images/ruler.png create mode 100644 core/modules/layout/js/layout.js diff --git a/core/modules/layout/css/layout.base.css b/core/modules/layout/css/layout.base.css new file mode 100644 index 0000000..316f804 --- /dev/null +++ b/core/modules/layout/css/layout.base.css @@ -0,0 +1,65 @@ +#layout-previewer-container { + box-shadow: 0 0 10px 0 black; + height: 100%; + left: -200%; + position: absolute; + width: 100%; + z-index: 1500; +} +#layout-previewer-container.active { + left: 0; +} +#layout-previewer-container .modal-background { + background-color: black; + bottom: 0; + height: 100%; + left: 0; + position: absolute; + right: 0; + top: 0; + width: 100%; + z-index: 1; +} +#frame-slider { + z-index: 50; +} +#frame-slider.ui-slider-horizontal .ui-slider-handle { + border: 0; + border-left: 1px solid #b0b0b0; + border-radius: 0; + height: 100%; + margin-left: 0; + top: 0; +} +#frame-slider.ui-slider-horizontal .ui-slider-handle + .ui-slider-handle { + border-left: 0 none; + border-right: 1px solid #b0b0b0; + margin-left: -1.2em; +} +#layout-previewer-container iframe { + height: 100%; + position: relative; + width: 100%; + z-index: 25; +} +#layout-size-input { + font-size: 0.75em; + position: absolute; + top: 0; + z-index: 100; +} +#layout-size-input * { + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; + display: inline-block; + float: left; +} +#layout-size-input .label { +} +#layout-size-input input { + padding: 0; + width: 10em; +} +#layout-size-input .unit { +} diff --git a/core/modules/layout/css/layout.theme.css b/core/modules/layout/css/layout.theme.css new file mode 100644 index 0000000..3b218d3 --- /dev/null +++ b/core/modules/layout/css/layout.theme.css @@ -0,0 +1,23 @@ +#frame-slider { + background-attachment: scroll; + background-color: #fcfcfc; + background-image: url("../images/ruler.png"); + background-position: center bottom; + background-repeat: repeat-x; + border-color: #ccc; + border-style: solid; + border-radius: 0; + border-width: 1px 0 0; + height: 2em; +} +#frame-slider .ui-widget-header { + background: none; + background-color: rgba(60,60,60,0.2); +} +#frame-slider.ui-slider-horizontal .ui-slider-handle { + background-attachment: scroll; + background-color: transparent; + background-image: url("../images/handle.png"); + background-position: center 1px; + background-repeat: no-repeat; +} diff --git a/core/modules/layout/images/handle.png b/core/modules/layout/images/handle.png new file mode 100644 index 0000000..656b884 --- /dev/null +++ b/core/modules/layout/images/handle.png @@ -0,0 +1,4 @@ +PNG + + IHDR&(ۙtEXtSoftwareAdobe ImageReadyqe<IDATxڤ9Aa?@B!$8 82bZ$q_fke[\AS~˛/@z"hEI'ėXFov .1"2(=ܻ /9LX_USB.uXsA{VX`˅xc0Еd l6q8D"i<k^ )8뽿%&x5 # 0#LN'%r9jQy8G;>a.K1R9L:|Bp'v z" F-|Hv4ᕛTnQXr;C9VT.-# GܨT*r@Q^Fd9sR)lZV (ك nZ4>H=n^ KaPW)UFWCRD?wFȃS:(pjRkUV.JCb^+5Ul60ը&()n*{T/iZLcͩ GĮ:ő +e>:T-{*zVmи=l| !&JIJcTŭ^lTnRT. >]C`ci( et 2(M^L͕ɒ{R@ҤAcӎwG1:$ ?*M=bIENDB` \ No newline at end of file diff --git a/core/modules/layout/images/ruler.png b/core/modules/layout/images/ruler.png new file mode 100644 index 0000000..f094c63 --- /dev/null +++ b/core/modules/layout/images/ruler.png @@ -0,0 +1,7 @@ +PNG + + IHDRZ )#yMtEXtSoftwareAdobe ImageReadyqe<IDATxVQ %z=PK +bpvghBRO_cL0MC,}έ=MS,K+ rԮThV@s҈ot366Cʟi _ksx^sy R,i.[;QK؆8t8M6ťq}p=Ӛ^L9/@RcKѳ(5b] '†2G36vT@@} ON:!mD +tY= +܆r}> 1 +1]-PV)th7!Jh+NWN}οS¸8VW;5{ҡ͛y!h69Gṁ3`^n7чNZO!}d9>} +0}_tQIENDB` \ No newline at end of file diff --git a/core/modules/layout/js/layout.js b/core/modules/layout/js/layout.js new file mode 100644 index 0000000..e246d76 --- /dev/null +++ b/core/modules/layout/js/layout.js @@ -0,0 +1,355 @@ +(function (Drupal, $) { + + Drupal.layout = Drupal.layout || {}; + + var size; + var handles = []; + var leftOffset; + var $frame; + var $slider; + var $container; + var $sizeInput; + var active = false; + + Drupal.behaviors.layout = { + attach: function (context, settings) { + var $body = $(window.top.document.body).once('layout-preview'); + + if ($body.length) { + // Set up the trigger link. + $('#toolbar-tab-layout_preview').on('click.layout', Drupal.layout.toggleLayoutPreview); + } + // Remove administrative elements in the document inside the iframe. + if (window.top !== window.self) { + $(context) + .find('body') + .removeClass('toolbar-tray-open') + .find('#toolbar-administration') + .remove(); + } + } + }; + + /** + * + */ + Drupal.layout.toggleLayoutPreview = function (event) { + // Build the previewer if it doesn't exist. + if (!$container) { + // Size is the width of the iframe. + size = size || window.top.document.documentElement.clientWidth; + // Initialize the handle positions. + handles = (handles.length) ? handles : [0, document.documentElement.clientWidth]; + Drupal.layout.buildPreviewer(); + } + $container.toggleClass('active'); + }; + + /** + * + */ + Drupal.layout.buildPreviewer = function (width) { + $(window.top.document.body).once('layout-preview-container', function (index, element) { + $container = $(Drupal.theme('layoutContainer')) + .appendTo(window.top.document.body); + $frame = $(Drupal.theme('layoutFrame')) + .css({ + 'width': size + }) + .appendTo($container); + }); + // Slider. + $slider = $('
') + .slider({ + 'animate': 'fast', + 'range': true, + 'max': document.documentElement.clientWidth, + 'min': 0, + 'values': handles, + 'slide': Drupal.layout.handleSlide + }) + .prependTo($container); + // Width label. + $sizeInput = $(Drupal.theme('layoutSizeInput')) + .appendTo($container); + // Displace the top of the container. + $container + .css({ + top: Drupal.layout.getDisplacement('top'), + }) + .attr('data-offset-top', Drupal.overlay.getDisplacement('top')); + // The contentDocument property is not supported in IE until IE8. + var iframeDocument = $frame[0].contentDocument || $frame[0].contentWindow.document; + + $(window.top).on('resize.layout', Drupal.layout.handleWindowResize); + $sizeInput.on('keypress.layout', {pattern: /^[0-9\.]$/, callback: Drupal.layout.handleSizeInputChange}, keyManager); + $container.on('sizeUpdate.layout', refreshPreviewSizing); + + // Trigger a resize to kick off some initial placements. + $(window.top).triggerHandler('resize.layout'); + + // Load the current page in the iframe. + // @todo, the location.href shouldn't be passed in naked like this. + iframeDocument.location.href = document.location.href; + }; + + /** + * + */ + Drupal.layout.handleSlide = function (event, ui) { + // Layout will control the placement of the handles. + event.preventDefault(); + var delta = 0; + var vals = []; + var handle, split; + // Get the delta of the original value of the handles and the new value. + for (var i = ui.values.length - 1; i >= 0; i--) { + // Get the original handle value. + var handle = handles[i]; + // The new handle value. + var value = ui.values[i]; + // If the original and the new values are not equal, adjust the handles. + if (handle !== value) { + var max = $slider.slider('option', 'max'); + var otherHandle = (i === 0) ? 1 : 0; + vals[i] = value; + // The value of the other handle is the inverse of the percentage of the + // active handle. + vals[otherHandle] = max * (1 - (value / max)); + // Update the dimensions variables in the closure. + updateDimensions(vals, max); + // Only one handle moves at a time, so if a handle-move was processed, + // then break; + break; + } + } + }; + + /** + * + */ + Drupal.layout.handleSizeInputChange = function (event) { + var newSize; + if (event.isDefaultPrevented()) { + return false; + } + if (event.key) { + newSize = $sizeInput.find('input').val(); + // If the key is a '.' and the value already contains one then + // prevent default. + if (event.key == '.' && newSize.indexOf('.') > -1) { + event.preventDefault(); + } + } + // Process the press of the enter key. + if (event.keyCode === 13) { + var newSize = parseFloat($sizeInput.find('input').val()); + if (newSize > 0) { + var max = $slider.slider('option', 'max'); + var vals = []; + var gutterPercent = (1 - (newSize / max)) / 2; + vals[0] = gutterPercent * max; + vals[1] = (gutterPercent * max) + newSize; + // Update the dimensions variables in the closure. + updateDimensions(vals, max, 250); + } + } + }; + + /** + * + */ + var updateDimensions = function (values, max, speed) { + // Store the new values of the handles. + handles = [values[0], values[1]]; + // Set the new size of the frame. + size = max * ((values[1] - values[0]) / max); + // Set the left offset of the frame. + leftOffset = max * (values[0] / max); + // Trigger a dimension change. + $container.trigger('sizeUpdate.layout', speed); + }; + + /** + * + */ + Drupal.layout.handleWindowResize = function (event) { + var doc = this.document.documentElement; + var docWidth = doc.clientWidth; + var framePercent = size / docWidth; + var gutterPercent = (1 - framePercent) / 2; + // Adjust the parameters of the slider. + $slider + // The new max of the slider is the width of the document. + .slider('option', 'max', docWidth) + // Update the position values of the slider handles. + .slider('values', [(gutterPercent * docWidth), ((gutterPercent + framePercent) * docWidth)]); + // If the window has been reduced below the width of the frame, reduce the + // width of the frame. + size = (docWidth < size) ? docWidth : size; + // Adjust the parameters of the frame. + $frame.css({ + 'left': gutterPercent * docWidth, + 'width': size + }); + // Adjust the position of the size input. + var inputPercent = $sizeInput.width() / docWidth; + gutterPercent = (1 - inputPercent) / 2; + $sizeInput.css({ + 'left': gutterPercent * docWidth + }); + // Update the size input value. + $sizeInput.find('input').val(size); + }; + + /** + * + */ + var refreshPreviewSizing = function (event, speed) { + speed = speed || 0; + // Adjust the frame. + $slider.slider('option', 'values', [handles[0], handles[1]]); + $frame.animate({ + left: leftOffset, + width: size + }, speed); + // Update the size input value. + $sizeInput.find('input').val(size); + }; + + /** + * Get the total displacement of given region. + * + * @param region + * Region name. Either "top" or "bottom". + * + * @return + * The total displacement of given region in pixels. + */ + Drupal.layout.getDisplacement = function (region) { + var displacement = 0; + var lastDisplaced = $('[data-offset-' + region + ']'); + if (lastDisplaced.length) { + displacement = parseInt(lastDisplaced.attr('data-offset-' + region)); + } + return displacement; + }; + + $.extend(Drupal.theme, { + /** + * Theme function to create the overlay iframe element. + */ + layoutContainer: function () { + return '
'; + }, + + /** + * Theme function to create an overlay iframe element. + */ + layoutFrame: function (url) { + return ''; + }, + + /** + * + */ + layoutSizeInput: function () { + return '
' + Drupal.t('Width') + 'px
'; + } + }); + + /** + * + */ + function keyManager (event) { + event.data = event.data || {}; + var pattern = event.data.pattern || undefined; + var callback = event.data.callback || undefined; + var controls = event.data.controls || [ + 8, // Delete. + 13, // Enter. + 37, // Left. + 38, // Up. + 39, // Right. + 40 // Down. + ]; + // Get the key from its keyCode. + var key = mapKeyToChar(event.shiftKey, event.keyCode); + // Prevent default if: + // (1) mapKeyToChar did not produce a key and the key is not a control key. + // (2) mapKeyToChar produced a key and the pattern does not match. + if ((!key && $.inArray(event.keyCode, controls) === -1) || + (key && (pattern && typeof pattern === 'object' && 'exec' in pattern && !pattern.exec(key)))) { + event.preventDefault(); + } + // Provide the key as the mapped character in the event object. + event.key = (key) ? key : null; + // The callback function should check for isDefaultPrevented() to know if + // the keyManager validated this key. + if (callback && typeof callback === 'function') { + callback.apply(this, arguments); + } + } + + /** + * + */ + function mapKeyToChar(isShifted, keyCode) { + if (keyCode === 27 + || keyCode === 8 + || keyCode === 9 + || keyCode === 20 + || keyCode === 16 + || keyCode === 17 + || keyCode === 91 + || keyCode === 13 + || keyCode === 92 + || keyCode === 18) { + return false; + } + if (typeof isShifted != "boolean" || typeof keyCode != "number") { + return false; + } + var charMap = []; + charMap[192] = "~"; + charMap[49] = "!"; + charMap[50] = "@"; + charMap[51] = "#"; + charMap[52] = "$"; + charMap[53] = "%"; + charMap[54] = "^"; + charMap[55] = "&"; + charMap[56] = "*"; + charMap[57] = "("; + charMap[48] = ")"; + charMap[109] = "_"; + charMap[107] = "+"; + charMap[219] = "{"; + charMap[221] = "}"; + charMap[220] = "|"; + charMap[59] = ":"; + charMap[222] = "\""; + charMap[188] = "<"; + charMap[190] = ">"; + charMap[191] = "?"; + charMap[32] = " "; + var character = ""; + if (isShifted) { + if (keyCode >= 65 && keyCode <= 90) { + character = String.fromCharCode(keyCode); + } + else { + character = charMap[keyCode]; + } + } + else { + if (keyCode >= 65 && keyCode <= 90) { + character = String.fromCharCode(keyCode).toLowerCase(); + } + else { + character = String.fromCharCode(keyCode); + } + } + return character; + } +}(Drupal, jQuery)); diff --git a/core/modules/layout/layout.module b/core/modules/layout/layout.module index c6ed7ae..321face 100644 --- a/core/modules/layout/layout.module +++ b/core/modules/layout/layout.module @@ -80,3 +80,66 @@ function layout_theme($existing, $type, $theme, $path) { } return $items; } + +/** + * Implements hook_toolbar(). + */ +function layout_toolbar() { + $items = array(); + + $items['layout_preview'] = array( + 'tab' => array( + 'title' => t('Layout preview'), + 'href' => '', + 'html' => FALSE, + 'attributes' => array( + 'title' => t('Preview page layout'), + 'id' => 'layout-previewer', + ), + ), + 'tray' => array( + '#attached' => array( + 'library' => array( + array('layout', 'layout.previewer'), + ), + ), + ), + 'weight' => 200, + ); + + return $items; +} + +/** + * Implements hook_library(). + */ +function layout_library_info() { + $libraries = array(); + $path = drupal_get_path('module', 'layout'); + $options = array( + 'scope' => 'footer', + 'attributes' => array('defer' => TRUE), + ); + + $libraries['layout.previewer'] = array( + 'title' => 'Preview layouts', + 'website' => 'http://drupal.org/project/layout', + 'version' => VERSION, + 'css' => array( + $path . '/css/layout.base.css', + $path . '/css/layout.theme.css', + ), + 'js' => array( + // Core. + $path . '/js/layout.js' => $options, + ), + 'dependencies' => array( + array('system', 'jquery'), + array('system', 'drupal.ajax'), + array('system', 'drupalSettings'), + array('system', 'jquery.ui.slider'), + ), + ); + + return $libraries; +} -- 1.7.10.4