diff --git a/core/modules/layout/config/layout.devices.yml b/core/modules/layout/config/layout.devices.yml new file mode 100644 index 0000000..da246a4 --- /dev/null +++ b/core/modules/layout/config/layout.devices.yml @@ -0,0 +1,21 @@ +devices: + iphone: + label: iPhone + dimensions: + width: 320 + height: 480 + android: + label: Android + dimensions: + width: 540 + height: 960 + ipad: + label: iPad + dimensions: + width: 768 + height: 1024 + desktop: + label: desktop + dimensions: + width: 1366 + height: 768 diff --git a/core/modules/layout/css/layout.base.css b/core/modules/layout/css/layout.base.css index 316f804..475e994 100644 --- a/core/modules/layout/css/layout.base.css +++ b/core/modules/layout/css/layout.base.css @@ -1,25 +1,90 @@ +/** + * Toolbar tab. + */ +.layout-preview-toolbar-tab { + display: none; +} +.js .toolbar .bar .layout-preview-toolbar-tab.tab { + display: block; + float: right; /* LTR */ +} + #layout-previewer-container { box-shadow: 0 0 10px 0 black; + display: none; height: 100%; left: -200%; position: absolute; width: 100%; - z-index: 1500; + z-index: 1050; } #layout-previewer-container.active { + display: block; left: 0; } -#layout-previewer-container .modal-background { +.layout-modal-background { background-color: black; + background-color: rgba(0,0,0,0.92); bottom: 0; height: 100%; left: 0; - position: absolute; + position: fixed; right: 0; - top: 0; + top: 3em; width: 100%; z-index: 1; } +.layout-slider-background { + background-color: white; + border: 1px solid white; + height: 2em; + position: absolute; + top: 1px; + width: 100%; + z-index: 2; +} +#layout-bp-view { + background-color: rgba(150,150,150,0.6667); + height: 2em; + position: absolute; + top: 2px; + width: 100%; + z-index: 5; +} +.layout-bp { + height: 2em; + position: absolute; + left: auto; + top: 0; + width: 100%; +} +.layout-bp div { + box-sizing: border-box; + height: 2em; + margin: 0 auto; + width: 100%; +} +.layout-bp-min { + background-color: rgba(255,255,255,0.75); + border-color: white; + border-style: solid; + border-width: 0 1px; + position: relative; +} +.layout-bp .label { + box-sizing: border-box; + display: block; + font-size: 0.7em; + height: 1em; + left: -100%; + line-height: 1; + padding-right: 6px; + position: absolute; + text-align: right; + top: 0; + white-space: nowrap; + width: 100%; +} #frame-slider { z-index: 50; } @@ -42,12 +107,15 @@ width: 100%; z-index: 25; } -#layout-size-input { - font-size: 0.75em; +#layout-previewer-controls { position: absolute; - top: 0; + top: 1em; z-index: 100; } +#layout-size-input { + float: left; + font-size: 0.75em; +} #layout-size-input * { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; @@ -63,3 +131,37 @@ } #layout-size-input .unit { } +#layout-devices-list { + display: block; + float: left; +} +/* Toolbar */ +.js .layout-preview-toolbar-tab .dropbutton-wrapper { + min-width: 6em; +} +.js .layout-preview-toolbar-tab .dropbutton-widget { + right: 0; +} +.js .layout-preview-toolbar-tab .dropbutton-widget .trigger { + text-align: right; +} +.js .layout-preview-toolbar-tab .dropbutton-widget .trigger a { + display: inline-block; + text-indent: 9999px; /* LTR */ +} +.layout-preview-toolbar-tab .dropbutton-toggle { + bottom: auto; + height: 3em; +} +.js .layout-preview-toolbar-tab .dropbutton-widget .dropbutton { + overflow: visible; +} + +/* Override Toolbar styling */ +.layout-preview-frame #toolbar-administration { + display: none !important; +} +body.toolbar-tray-open.layout-preview-frame { + margin-left: 0 !important; + margin-right: 0 !important; +} diff --git a/core/modules/layout/css/layout.theme.css b/core/modules/layout/css/layout.theme.css index 3b218d3..ceb3d90 100644 --- a/core/modules/layout/css/layout.theme.css +++ b/core/modules/layout/css/layout.theme.css @@ -1,6 +1,13 @@ +/** + * Toolbar tab. + */ +.layout-preview-toolbar-tab .layout-previewer-options { + background-color: #0f0f0f; +} + #frame-slider { background-attachment: scroll; - background-color: #fcfcfc; + background-color: transparent; background-image: url("../images/ruler.png"); background-position: center bottom; background-repeat: repeat-x; @@ -10,9 +17,9 @@ border-width: 1px 0 0; height: 2em; } -#frame-slider .ui-widget-header { +#frame-slider .ui-slider-range { background: none; - background-color: rgba(60,60,60,0.2); + background-color: rgba(90,90,90,0.2); } #frame-slider.ui-slider-horizontal .ui-slider-handle { background-attachment: scroll; @@ -21,3 +28,70 @@ background-position: center 1px; background-repeat: no-repeat; } + +/** + * Toolbar. + */ +.toolbar .bar .icon.icon-layout { + margin-left: 0; + margin-right: 0; + padding-left: 0; + padding-right: 0; + text-indent: -9999px; + width: 4em; +} +.icon-layout:before { + background-image: url("../images/icon-layout.png"); +} +.toolbar .bar .layout-preview-toolbar-tab .icon-layout:before { + left: 1.75em; +} +.layout-preview-toolbar-tab .open .icon-layout:before, +.layout-preview-toolbar-tab .icon-layout.active:before { + background-image: url("../images/icon-layout-active.png"); +} +.js .layout-preview-toolbar-tab .dropbutton-widget, +.js .layout-preview-toolbar-tab .dropbutton-widget:hover, +.js .layout-preview-toolbar-tab .dropbutton-multiple.open .dropbutton-widget:hover { + background-color: transparent; + border: none; +} +.layout-preview-toolbar-tab .open .dropbutton { + box-shadow: 1em -1em 1em 1em rgba(0, 0, 0, 0.75); +} +.js .layout-preview-toolbar-tab .open .dropbutton li + li { + background-color: white; +} +.js .layout-preview-toolbar-tab .dropbutton a { + color: black; +} +.layout-preview-toolbar-tab .layout-previewer-options { + border-right: none; +} +.layout-preview-toolbar-tab .layout-previewer-options a { + padding-bottom: 1em; + padding-top: 1em; +} +.layout-preview-toolbar-tab .dropbutton-toggle button { + color: #a0a0a0; +} +.layout-preview-toolbar-tab .open .dropbutton-toggle button { + color: black; +} +.layout-preview-toolbar-tab .dropbutton-arrow { + border-width: 0.75em 0.5em 0; +} +.layout-preview-toolbar-tab .dropbutton-multiple.open .dropbutton-arrow { + border-bottom: 0.75em solid; + border-top-color: transparent; +} +.layout-preview-toolbar-tab .dropbutton .secondary-action { + border-top: none; + margin-right: -2em; +} +.layout-preview-toolbar-tab .dropbutton .secondary-action a { + margin-right: 0; +} +.layout-preview-toolbar-tab .dropbutton .additional-options { + border-top: 1px solid #cfcfcf; +} diff --git a/core/modules/layout/images/icon-layout-active.png b/core/modules/layout/images/icon-layout-active.png new file mode 100644 index 0000000..ff89708 --- /dev/null +++ b/core/modules/layout/images/icon-layout-active.png @@ -0,0 +1,14 @@ +PNG + + IHDR $ pHYs  ~ +OiCCPPhotoshop ICC profilexڝSgTS=BKKoR RB&*! J!QEEȠQ, +!{kּ> H3Q5 B.@ +$pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB +dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ +b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| +J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ +M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% +yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- +BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3- cHRMz%u0`:o_FkIDATx쓱 0 $1(Rd) 4 7)'Y./-$1TܝÓR7wohkSM;L!FgZk 'x|S댞 |IENDB` \ No newline at end of file diff --git a/core/modules/layout/images/icon-layout.png b/core/modules/layout/images/icon-layout.png new file mode 100644 index 0000000..d1d6aef --- /dev/null +++ b/core/modules/layout/images/icon-layout.png @@ -0,0 +1,14 @@ +PNG + + IHDR $ pHYs  ~ +OiCCPPhotoshop ICC profilexڝSgTS=BKKoR RB&*! J!QEEȠQ, +!{kּ> H3Q5 B.@ +$pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB +dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ +b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| +J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ +M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% +yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- +BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3- cHRMz%u0`:o_FIDATx >RK*p B% 1 -1]-PV)th7!Jh+NWN}οS¸8VW;5{ҡ͛y!h69Gṁ3`^n7чNZO!}d9>} -0}_tQIENDB` \ No newline at end of file + IHDRZ )#yMtEXtSoftwareAdobe ImageReadyqe<CIDATxױ AuXWXTB®NfF_thC;"] VIENDB` \ No newline at end of file diff --git a/core/modules/layout/js/layout.js b/core/modules/layout/js/layout.js index e246d76..e165d60 100644 --- a/core/modules/layout/js/layout.js +++ b/core/modules/layout/js/layout.js @@ -2,30 +2,37 @@ Drupal.layout = Drupal.layout || {}; - var size; - var handles = []; - var leftOffset; - var $frame; - var $slider; - var $container; - var $sizeInput; - var active = false; + var $toolbarTab = $(); + var size; // The width of the preview container. + var handles = []; // The values of the jQuery UI Slider handles. + var leftOffset; // The left value of the iframe containing the previewed page. + var $frame; // The iframe that contains the previewed page. + var iframeDocument; // The document of the iframe that contains the preview. + var $slider; // The jQuery UI Slider widget that adjusts the iframed preview. + var $container; // The container of the page preview component. + var $sizeInput; // The input element that display the width of the preview. + var breakpoints = {}; // A list of breakpoints, keyed by configuration string. + var $breakpointView; // The container of the breakpoint views. 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); + // Append the selector to the preview container. + $toolbarTab = $('.layout-preview-toolbar-tab') + .on('click.layout', '#layout-previewer', loadDefaultPreview) + .on('click.layout', '.layout-preview-device', loadDevicePreview); + // Register a handler on window resize to reposition the tab dropdown. + $(window.top) + .on('resize.layout.tab', handleWindowToolbarResize); } // 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(); + var $frameBody = $(window.self.document.body).once('layout-preview'); + if ($frameBody.length > 0) { + $frameBody.get(0).className += ' layout-preview-frame'; + } } } }; @@ -33,22 +40,40 @@ /** * */ - Drupal.layout.toggleLayoutPreview = function (event) { + var toggleConfigurationOptions = function (event) { + event.preventDefault(); + var open = (event.data && typeof event.data.open === 'boolean') ? event.data.open : undefined; + $(event.delegateTarget) + .find('.layout-previewer-options') + .toggle(open) + .correctEdgeCollisions(); + } + + /** + * Toggles the layout preview component on or off. + * + * When first toggled on, the layout preview component is built. All + * subsequent toggles hide or show the build component. + * + * @param Object Event + * - jQuery Event object. + */ + var toggleLayoutPreview = function (activate) { // 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(); + buildPreviewer(); + // Size is the width of the iframe. + updateDimensions({width: (size || window.top.document.documentElement.clientWidth)}); } - $container.toggleClass('active'); + $container.toggleClass('active', activate); }; /** - * + * Assembles a layout previewer. */ - Drupal.layout.buildPreviewer = function (width) { + var buildPreviewer = function () { $(window.top.document.body).once('layout-preview-container', function (index, element) { $container = $(Drupal.theme('layoutContainer')) .appendTo(window.top.document.body); @@ -57,51 +82,65 @@ 'width': size }) .appendTo($container); + // Slider. + $slider = $(Drupal.theme('layoutSlider')) + .slider({ + 'animate': 'fast', + 'range': true, + 'max': document.documentElement.clientWidth, + 'min': 0, + 'values': handles, + 'slide': handleSlide + }) + .prependTo($container); + + // Load the breakpoints for the current theme. + if ('breakpoints' in Drupal.settings.layout.routes) { + $.ajax(Drupal.settings.layout.routes.breakpoints) + .success(breakpointsCallback); + } + // Attach controls + $container.append(Drupal.theme('layoutControls')); + // Width label. + $sizeInput = $(Drupal.theme('layoutSizeInput')) + .appendTo($container.find('#layout-previewer-controls')); + // Displace the top of the container. + $container + .css({ + top: getDisplacement('top'), + }) + .attr('data-offset-top', getDisplacement('top')); + // The contentDocument property is not supported in IE until IE8. + iframeDocument = $frame[0].contentDocument || $frame[0].contentWindow.document; + + $(window.top).on('resize.layout', handleWindowResize); + $sizeInput.on('keypress.layout', {pattern: /^[0-9\.]$/, callback: 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 URI into the preview iframe. + // @todo, are there any security implications to loading a page like this? + iframeDocument.location.href = Drupal.settings.basePath + Drupal.settings.currentPath; }); - // 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; }; /** + * Responds to a jQuery UI Slider slide event. * + * @param Object Event + * - jQuery Event object. + * + * @param Object ui + * - jQuery Slider widget state information resulting from a slide event. */ - Drupal.layout.handleSlide = function (event, ui) { + var handleSlide = function (event, ui) { // Layout will control the placement of the handles. event.preventDefault(); var delta = 0; var vals = []; - var handle, split; + var handle, split, width; // 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. @@ -116,8 +155,10 @@ // 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); + // Get the updated width of the viewport. + width = Math.abs(vals[i] - vals[otherHandle]); + // Update the dimension variables in the closure. + updateDimensions({width: width}); // Only one handle moves at a time, so if a handle-move was processed, // then break; break; @@ -126,9 +167,12 @@ }; /** + * Responds to keypress events from the frame size input. * + * @param Object Event + * - jQuery Event object. */ - Drupal.layout.handleSizeInputChange = function (event) { + var handleSizeInputChange = function (event) { var newSize; if (event.isDefaultPrevented()) { return false; @@ -145,25 +189,41 @@ 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); + updateDimensions({width: newSize}, 250); } } }; /** + * Updates the dimension variables of the previewer components. * + * @param Array values + * - An array that contains the position values of the handles of the jQuery + * UI slider. + * + * @param max + * - The maximum width of the previewer. Often this is just the width of the + * client. + * + * @param speed + * - A number representing time in milliseconds or a jQuery speed keyword. + * Determines the speed at which animations between changes dimension values + * should take place. Defaults to zero. */ - var updateDimensions = function (values, max, speed) { + var updateDimensions = function (dimensions, speed) { + var width = dimensions.width || NaN; + var height = dimensions.height || NaN; + // Calculate the handle placements. + var max = $slider.slider('option', 'max'); + var values = []; + var gutterPercent = (1 - (width / max)) / 2; + values[0] = gutterPercent * max; + values[1] = (gutterPercent * max) + width; // 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); + size = Math.round((max * ((values[1] - values[0]) / max)) * 10) / 10; // Set the left offset of the frame. leftOffset = max * (values[0] / max); // Trigger a dimension change. @@ -173,7 +233,19 @@ /** * */ - Drupal.layout.handleWindowResize = function (event) { + var handleWindowToolbarResize = function (event) { + $toolbarTab + .find('.layout-previewer-options') + .correctEdgeCollisions(); + }; + + /** + * Responds to window resize events. + * + * @param Object Event + * - jQuery Event object. + */ + var handleWindowResize = function (event) { var doc = this.document.documentElement; var docWidth = doc.clientWidth; var framePercent = size / docWidth; @@ -186,7 +258,7 @@ .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; + updateDimensions({width: ((docWidth < size) ? docWidth : size)}); // Adjust the parameters of the frame. $frame.css({ 'left': gutterPercent * docWidth, @@ -195,7 +267,7 @@ // Adjust the position of the size input. var inputPercent = $sizeInput.width() / docWidth; gutterPercent = (1 - inputPercent) / 2; - $sizeInput.css({ + $container.find('#layout-previewer-controls').css({ 'left': gutterPercent * docWidth }); // Update the size input value. @@ -205,6 +277,157 @@ /** * */ + $.fn.correctEdgeCollisions = (function () { + + function correct () { + // Clear any previous corrections. + clear.apply(this); + // Go through each element and correct edge collisions. + return this.each(function (index, element) { + var $this = $(this); + var width = $this.width(); + var height = $this.height(); + var clientW = document.documentElement.clientWidth; + var clientH = document.documentElement.clientHeight; + var collisions = { + 'top': null, + 'right': null, + 'bottom': null, + 'left': null + }; + // Determine if the element is too big for the document. Resize to fit. + if (width > clientW) { + $this.width(clientW); + // If the element is too wide, it will collide on both left and right. + collisions.left = true; + collisions.right = true; + } + if (height > clientH) { + $this.height(clientH); + // If the element is too high, it will collide on both top and bottom. + collisions.top = true; + collisions.bottom = true; + } + // Check each edge for a collision. + if (!collisions.top && $this.offset().top < 0) { + collisions.top = true; + } + if (!collisions.right && (($this.offset().left + width) > clientW)) { + collisions.right = true; + } + if (!collisions.bottom && (($this.offset().top + height) > clientH)) { + collisions.bottom = true; + } + if (!collisions.left && $this.offset().left < 0) { + collisions.left = true; + } + // Set the offset to zero for any collision on an edge. + for (var edge in collisions) { + if (collisions.hasOwnProperty(edge)) { + if (collisions[edge]) { + $this.css(edge, 0); + } + } + } + }); + } + + function clear () { + var edges = ['top', 'right', 'bottom', 'left']; + return this.each(function (index, element) { + for (var i = 0; i < edges.length; i++) { + this.style[edges[i]] = ""; + } + }); + } + + + var methods = { + 'correct': correct, + 'clear': clear + }; + + return function (method) { + // Method calling logic + if (methods[method]) { + return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); + } else if (typeof method === 'object' || ! method) { + return methods.correct.apply(this, arguments); + } else { + $.error(Drupal.t('Method @method does not exist in this plugin.', {'@method': method})); + } + }; + + }()); + + /** + * Renders breakpoint configuration to an HTML view. + * + * @param Object data + * - Breakpoint configuration data. The keys of the object correspond to the + * keys of theme-configured breakpoints. The value of each key is a string + * that represents a media query. + * + * @param String textStatus + * - The status of the AJAX request. + * + * @param Object jqXHR + * - A jQuery XMLHttpRequest object. + */ + var breakpointsCallback = function (data, textStatus, jqXHR) { + $breakpointView = $(Drupal.theme('layoutBreakpointView')); + var $item, options; + for (var bp in data) { + if (data.hasOwnProperty(bp)) { + breakpoints[bp] = parseMediaQuery(data[bp]); + // Append a representation of each breakpoint to the frame slider. + if (breakpoints[bp].hasquery) { + options = breakpoints[bp]; + options.id = 'layout-bp-' + bp.replace(/\./g, '-'); + options.label = bp; + $item = $(Drupal.theme('layoutBreakpointItemView', options)); + $breakpointView.prepend($item); + } + } + } + if ($breakpointView.children().length) { + // @todo, These breakpoints should be sorted by minw eventually. For now + // we assume they are listed from small to largest. + $container.append($breakpointView); + } + }; + + /** + * + */ + function loadDefaultPreview (event) { + event.preventDefault(); + toggleLayoutPreview(); + updateDimensions({width: document.documentElement.clientWidth}, 250); + } + + /** + * + */ + function loadDevicePreview (event) { + event.preventDefault(); + toggleLayoutPreview(true); + var $link = $(event.target); + var width = $link.data('layout-width') + updateDimensions({width: width}, 250); + } + + /** + * Redraws the layout preview component based on the stored dimensions. + * + * @param Object event + * - A jQuery event object. + * + * @param Number/String speed + * - A number representing time in milliseconds or a jQuery speed keyword. + * Determines the speed at which animations between changes dimension values + * should take place. Defaults to zero. + */ var refreshPreviewSizing = function (event, speed) { speed = speed || 0; // Adjust the frame. @@ -220,13 +443,13 @@ /** * Get the total displacement of given region. * - * @param region + * @param String region * Region name. Either "top" or "bottom". * * @return * The total displacement of given region in pixels. */ - Drupal.layout.getDisplacement = function (region) { + var getDisplacement = function (region) { var displacement = 0; var lastDisplaced = $('[data-offset-' + region + ']'); if (lastDisplaced.length) { @@ -237,31 +460,83 @@ $.extend(Drupal.theme, { /** - * Theme function to create the overlay iframe element. + * Returns the preview container element. */ layoutContainer: function () { - return '
'; + return '
'; }, /** - * Theme function to create an overlay iframe element. + * Returns an overlay iframe element. */ layoutFrame: function (url) { return ''; }, /** + * Returns the HTML for the jQuery UI Slider attachment. + */ + layoutSlider: function () { + return '
'; + }, + + /** * */ + layoutControls: function () { + return '
'; + }, + + /** + * Returns the input element for changing the preview width. + */ layoutSizeInput: function () { return '
' + Drupal.t('Width') + 'px
'; + }, + + /** + * Returns the wrapper for breakpoint item views. + */ + layoutBreakpointView: function () { + return '
' + }, + + /** + * Returns individual breakpoint configuration views. + */ + layoutBreakpointItemView: function (options) { + var markup = ''; + options = options || {}; + + markup += '
'; + markup += '
'; + markup += '
' + options.label + '
'; + markup += '
'; + markup += '
'; + + return markup; } }); /** + * Handles key input. * + * Fires a callback function with either return the key that was pressed in + * the event.key property, or, if the key is a control key or does not match + * a supplied pattern, then null as the event.key value. + * + * @param Regex event.data.pattern + * - A regular expression that filters allowed key input. Only keys matching + * the expression will be returned. All other keys return null. + * + * @param Function event.data.callback + * - A callback function to be invoked after a key is processed. Any + * variadic parameters supplied to keyManager are passed through as well. + * + * @param Array event.data.controls + * - An Array of char codes that should be ignored as control keys. */ - function keyManager (event) { + var keyManager = function (event) { event.data = event.data || {}; var pattern = event.data.pattern || undefined; var callback = event.data.callback || undefined; @@ -292,9 +567,20 @@ } /** + * Maps keyCode to Strings, taking the shift key state into account. * + * @param Boolean isShifted + * - A Boolean representing if the shift key was pressed (true) or not + * (false) + * + * @param Number keyCode + * - The numeric code of the key that was pressed. + * + * @return String + * - A single character corresponding to the keyCode or null if no + * correspondence is found. */ - function mapKeyToChar(isShifted, keyCode) { + var mapKeyToChar = function (isShifted, keyCode) { if (keyCode === 27 || keyCode === 8 || keyCode === 9 @@ -352,4 +638,19 @@ } return character; } + + /** + * Parses a String representing a media query into usable values. + * + * @param String mq + * - A String representing a media query e.g. 'all and (min-width: 800px)' + */ + var parseMediaQuery = function (mq) { + return { + media : mq.split("(")[0].match(/(only\s+)?([a-zA-Z]+)\s?/) && RegExp.$2 || "all", + hasquery: mq.indexOf("(") > -1, + minw : mq.match(/\(min\-width:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/) && parseFloat(RegExp.$1) + (RegExp.$2 || ""), + maxw : mq.match(/\(max\-width:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/) && parseFloat(RegExp.$1) + (RegExp.$2 || "") + } + } }(Drupal, jQuery)); diff --git a/core/modules/layout/layout.module b/core/modules/layout/layout.module index 321face..6cc9288 100644 --- a/core/modules/layout/layout.module +++ b/core/modules/layout/layout.module @@ -5,6 +5,8 @@ * Manages page layouts for content presentation. */ +use Symfony\Component\HttpFoundation\JsonResponse; + /** * Implements hook_menu(). */ @@ -25,6 +27,13 @@ function layout_menu() { 'access arguments' => array(4), 'file' => 'layout.admin.inc', ); + // Fetch breakpoints for a theme. + $items['layout/breakpoints'] = array( + 'page callback' => 'layout_retrieve_theme_breakpoints_jsonp', + 'access callback' => 'user_access', + 'access arguments' => array('administer layouts'), + 'type' => MENU_CALLBACK, + ); return $items; } @@ -78,33 +87,112 @@ function layout_theme($existing, $type, $theme, $path) { 'template' => $layout['template'], ); } + return $items; } /** + * + */ +function layout_preview_toolbar_controls() { + $links['trigger'] = array( + 'title' => t('Layout preview'), + 'href' => '', + 'attributes' => array( + 'id' => 'layout-previewer', + 'title' => "Preview page layout", + 'class' => array('icon', 'icon-layout'), + ), + 'weight' => 0, + ); + + $links += layout_get_devices_list(); + + $links['ruler'] = array( + 'title' => t('Show responsive ruler'), + 'href' => '', + 'attributes' => array( + 'title' => t('Show the responsive ruler above the page preview.'), + 'class' => array('additional-options'), + ), + 'weight' => 100, + ); + + return $links; +} + +/** + * Page callback: Returns the breakpoints of the current active theme. + * + * @see layout_menu(). + */ +function layout_get_devices_list() { + $devices = config('layout.devices')->get('devices'); + + $links = array(); + + foreach($devices as $name => $info) { + $links[$name] = array( + 'title' => $info['label'], + 'href' => '', + 'attributes' => array( + 'class' => array('layout-preview-device'), + 'data-layout-width' => ($info['dimensions']['width']) ? $info['dimensions']['width'] : '', + 'data-layout-height' => ($info['dimensions']['height']) ? $info['dimensions']['height'] : '', + ), + ); + } + return $links; +} + +/** + * Page callback: Returns the breakpoints of the current active theme. + * + * @see layout_menu(). + */ +function layout_retrieve_theme_breakpoints_jsonp() { + global $theme_key; + + // Get the configured breakpoint to switch from vertical to horizontal + // toolbar presentation. + $media_queries = array(); + $breakpoints = entity_load('breakpoint_group', 'theme.' . $theme_key . '.' . $theme_key); + if (!empty($breakpoints)) { + $media_queries = array_map( + function($object) { + return $object->mediaQuery; + }, + $breakpoints->breakpoints + ); + } + + $response = new JsonResponse($media_queries); + return $response; +} + +/** * Implements hook_toolbar(). */ function layout_toolbar() { - $items = array(); $items['layout_preview'] = array( + '#type' => 'toolbar_item', 'tab' => array( - 'title' => t('Layout preview'), - 'href' => '', - 'html' => FALSE, - 'attributes' => array( - 'title' => t('Preview page layout'), - 'id' => 'layout-previewer', + '#type' => 'dropbutton', + '#links' => layout_preview_toolbar_controls(), + '#attributes' => array( + 'class' => array('layout-previewer-options'), ), ), - 'tray' => array( - '#attached' => array( - 'library' => array( - array('layout', 'layout.previewer'), - ), + '#wrapper_attributes' => array( + 'class' => array('layout-preview-toolbar-tab'), + ), + '#attached' => array( + 'library' => array( + array('layout', 'layout.previewer'), ), ), - 'weight' => 200, + '#weight' => 200, ); return $items; @@ -132,6 +220,16 @@ function layout_library_info() { 'js' => array( // Core. $path . '/js/layout.js' => $options, + array( + 'data' => array( + 'layout' => array( + 'routes' => array( + 'breakpoints' => 'layout/breakpoints' + ), + ), + ), + 'type' => 'setting', + ), ), 'dependencies' => array( array('system', 'jquery'),