From fee4e75f4ef7c024469988bacb4e1bdd90096295 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?"J.=20Rene=CC=81e=20Beach"?= Date: Mon, 11 Feb 2013 00:30:09 -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 Signed-off-by: J. Renée Beach --- core/modules/layout/config/layout.devices.yml | 26 + core/modules/layout/css/layout.preview.base.css | 212 ++++++ core/modules/layout/css/layout.preview.theme.css | 97 +++ core/modules/layout/images/close.png | 7 + core/modules/layout/images/handle.png | 4 + core/modules/layout/images/icon-layout-active.png | 14 + core/modules/layout/images/icon-layout.png | 14 + core/modules/layout/images/ruler.png | 3 + core/modules/layout/js/layout.preview.js | 741 +++++++++++++++++++++ core/modules/layout/layout.install | 41 ++ core/modules/layout/layout.module | 208 ++++++ 11 files changed, 1367 insertions(+) create mode 100644 core/modules/layout/config/layout.devices.yml create mode 100644 core/modules/layout/css/layout.preview.base.css create mode 100644 core/modules/layout/css/layout.preview.theme.css create mode 100644 core/modules/layout/images/close.png create mode 100644 core/modules/layout/images/handle.png create mode 100644 core/modules/layout/images/icon-layout-active.png create mode 100644 core/modules/layout/images/icon-layout.png create mode 100644 core/modules/layout/images/ruler.png create mode 100644 core/modules/layout/js/layout.preview.js create mode 100644 core/modules/layout/layout.install diff --git a/core/modules/layout/config/layout.devices.yml b/core/modules/layout/config/layout.devices.yml new file mode 100644 index 0000000..b39722c --- /dev/null +++ b/core/modules/layout/config/layout.devices.yml @@ -0,0 +1,26 @@ +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 + bigscreen: + label: big screen + dimensions: + width: 1600 + height: 800 diff --git a/core/modules/layout/css/layout.preview.base.css b/core/modules/layout/css/layout.preview.base.css new file mode 100644 index 0000000..e2aaed5 --- /dev/null +++ b/core/modules/layout/css/layout.preview.base.css @@ -0,0 +1,212 @@ +.layout-preview-active { + height: 100%; + overflow: hidden; +} + +/** + * Toolbar tab. + */ +.layout-preview-toolbar-tab { + display: none; +} +.js .toolbar .bar .layout-preview-toolbar-tab.tab { + display: block; + float: left; +} +@media only screen and (min-width: 36em) { + .js .toolbar .bar .layout-preview-toolbar-tab.tab { + float: right; /* LTR */ + } +} +.layout-preview-toolbar-tab .layout-previewer-options { + display: none; +} +.layout-preview-toolbar-tab.open .layout-previewer-options { + display: block; +} + +#layout-previewer-container { + box-shadow: 0 0 10px 0 black; + display: none; + height: 100%; + left: -200%; + position: absolute; + width: 100%; + z-index: 1050; +} +#layout-previewer-container.active { + display: block; + left: 0; +} +#layout-preview-close { + background-attachment: scroll; + background-color: #a0a0a0; + background-image: url("../images/close.png"); + background-image: url("../images/close.png"), -webkit-linear-gradient(transparent, #787878 150%); + background-image: url("../images/close.png"), linear-gradient(transparent, #787878 150%); + background-position: center center; + background-repeat: no-repeat; + border: none; + border-radius: 3px; + cursor: pointer; + font-size: 1em; + height: 2.333em; + left: 0; + margin-left: 10px; + margin-top: 9px; + position: absolute; + text-indent: -9999em; /* LTR */ + width: 2.333em; + z-index: 75; +} +#layout-preview-close:hover { + background-image: url("../images/close.png"); +} +.layout-modal-background { + background-color: black; + background-color: rgba(0,0,0,0.92); + bottom: 0; + height: 100%; + left: 0; + position: fixed; + right: 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; +} +#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: 100; +} +#layout-previewer-controls { + display: none; + position: relative; + z-index: 25; +} +#layout-previewer-controls.active { + display: block; +} +#layout-size-input { + font-size: 0.75em; + position: absolute; + top: 1em; + z-index: 50; +} +#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 { +} +#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.preview.theme.css b/core/modules/layout/css/layout.preview.theme.css new file mode 100644 index 0000000..9b900ee --- /dev/null +++ b/core/modules/layout/css/layout.preview.theme.css @@ -0,0 +1,97 @@ +/** + * Toolbar tab. + */ +.layout-preview-toolbar-tab .layout-previewer-options { + background-color: #0f0f0f; +} + +#frame-slider { + background-attachment: scroll; + background-color: transparent; + 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-slider-range { + background: none; + background-color: rgba(90,90,90,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; +} + +/** + * Toolbar. + */ +.toolbar .bar .icon.icon-layout { + margin-left: 0; + margin-right: 0; + padding-left: 0; + padding-right: 0; + text-indent: -9999px; + width: 5em; +} +.icon-layout:before { + background-image: url("../images/icon-layout.png"); +} +.toolbar .bar .layout-preview-toolbar-tab .icon-layout:before { + left: 1em; +} +.layout-preview-toolbar-tab .open .icon-layout:before, +.layout-preview-toolbar-tab .icon-layout.active:before { + background-image: url("../images/icon-layout-active.png"); +} +@media only screen and (min-width: 16.5em) { + .toolbar .layout-preview-toolbar-tab.tab .icon-layout:before { + width: 20px; + } +} +.layout-preview-toolbar-tab .layout-previewer-options { + border-right: none; + box-shadow: 0 0 2em 0 rgba(0, 0, 0, 0.75); + position: absolute; +} +.layout-preview-toolbar-tab .layout-previewer-options li { + background-color: white; + border-top: 1px solid #cfcfcf; +} +.layout-preview-toolbar-tab .trigger, +.layout-preview-toolbar-tab .layout-previewer-options a { + padding-bottom: 1em; + padding-top: 1em; +} +.toolbar .layout-preview-toolbar-tab.tab .layout-previewer-options a { + color: black; +} +.layout-preview-toolbar-tab .trigger:after { + border-bottom-color: transparent; + border-left-color: transparent; + border-right-color: transparent; + border-style: solid; + border-width: 0.4545em 0.4em 0; + color: #a0a0a0; + content: ' '; + display: block; + height: 0; + line-height: 0; + position: absolute; + right: 1.2em; + top: 50%; + margin-top: -0.1666em; + width: 0; + overflow: hidden; +} +.layout-preview-toolbar-tab.open .trigger:after { + border-bottom: 0.4545em solid; + border-top-color: transparent; + top: 1.25em; +} diff --git a/core/modules/layout/images/close.png b/core/modules/layout/images/close.png new file mode 100644 index 0000000..2203cbb --- /dev/null +++ b/core/modules/layout/images/close.png @@ -0,0 +1,7 @@ +PNG + + IHDRatEXtSoftwareAdobe ImageReadyqe<IDATx|R=LZa}!T%MvpWt $kͩZ;Cn&j*1&jæE14`C=!{߹~]DLDwVWWMcccJrJcLA.Oe5 uIkJ>hXq7F"Z}^ +N~vR䜘l6|nZݲ,[4M# xa.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/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% 0) { + $frameBody.get(0).className += ' layout-preview-frame'; + } + } + } + }; + + /** + * + */ + var toggleConfigurationOptions = function (event) { + event.preventDefault(); + var open = (event.data && typeof event.data.open === 'boolean') ? event.data.open : undefined; + $(event.delegateTarget) + .toggleClass('open', open) + .find('.layout-previewer-options') + .drupalLayout('correctEdgeCollisions'); + }; + + /** + * + */ + var toggleControls = function (event) { + event.preventDefault(); + var $this = $(this); + $controls.toggleClass('active'); + if ($controls.hasClass('active')) { + $this.text(Drupal.t('Hide developer ruler')); + } + else { + $this.text(Drupal.t('Show developer ruler')); + } + } + + /** + * 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 (event, activate) { + event.preventDefault(); + // Build the previewer if it doesn't exist. + if (!$container) { + // Initialize the handle positions. + handles = (handles.length) ? handles : [0, document.documentElement.clientWidth]; + buildPreviewer(); + // Size is the width of the iframe. + updateDimensions({width: (size || window.top.document.documentElement.clientWidth)}); + } + $container + .toggleClass('active', activate) + $('body') + .toggleClass('layout-preview-active', activate); + }; + + /** + * Assembles a layout previewer. + */ + var buildPreviewer = function () { + $(window.top.document.body).once('layout-preview-container', function (index, element) { + $container = $(Drupal.theme('layoutContainer')); + // Build the previewer controls. + $controls = $(Drupal.theme('layoutControls')); + // Slider. + $slider = $(Drupal.theme('layoutSlider')) + .filter('#frame-slider') + .slider({ + 'animate': 'fast', + 'range': true, + 'max': document.documentElement.clientWidth, + 'min': 0, + 'values': handles, + 'slide': handleSlide + }) + .appendTo($controls); + + // Load the breakpoints for the current theme. + if ('breakpoints' in Drupal.settings.layout.routes) { + $.ajax(Drupal.settings.layout.routes.breakpoints) + .success(breakpointsCallback); + } + // Width label. + $sizeInput = $(Drupal.theme('layoutSizeInput')) + .appendTo($controls); + + // Attach the controls. + $container.append($controls); + + // Add a close button. + $container + .append(Drupal.theme('layoutClose')); + + // Attach the frame that will hold the preview. + $frame = $(Drupal.theme('layoutFrame')) + .css({ + 'width': size + }) + .appendTo($container); + + // Append the container to the window. + $container.appendTo(window.top.document.body); + // 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; + + $container + .on('click.layout', '#layout-preview-close', {activate: false}, toggleLayoutPreview) + .on('keypress.layout', '#layout-size-input', {pattern: /^[0-9\.]$/, callback: handleSizeInputChange}, keyManager) + .on('sizeUpdate.layout', refreshPreviewSizing); + + // Trigger a resize to kick off some initial placements. + $(window.top) + .on('resize.layout', handleWindowResize) + .trigger('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; + }); + }; + + /** + * 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. + */ + var handleSlide = function (event, ui) { + // Layout will control the placement of the handles. + event.preventDefault(); + var delta = 0; + var vals = []; + 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. + 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)); + // 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; + } + } + }; + + /** + * Responds to keypress events from the frame size input. + * + * @param Object Event + * - jQuery Event object. + */ + var 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) { + // Update the dimensions variables in the closure. + 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 (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; + // The gutters must be at least the width of the edgeTolerance + values[0] = (values[0] < edgeTolerance) ? edgeTolerance : values[0]; + // The frame width must fit within the different of the gutters and the page + // width. + width = (max - (values[0] * 2) < width) ? max - (values[0] * 2) : width; + values[1] = values[0] + width; + // Store the new values of the handles. + handles = [values[0], values[1]]; + // Set the new size of the frame. + 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. + $container.trigger('sizeUpdate.layout', speed); + }; + + /** + * + */ + var handleWindowToolbarResize = function (event) { + var options = $toolbarTab + .find('.layout-previewer-options') + // Move the list back onto the screen. + .drupalLayout('correctEdgeCollisions') + .find('.layout-preview-device') + // Hide layout options that are wider than the current screen + .drupalLayout('prunePreviewChoices', {tolerance: edgeTolerance}) + // The lis will be toggled. Assign them to options. + .parent('li'); + + $toolbarTab.toggle(options.not('.element-hidden').length > 0); + }; + + /** + * 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; + 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. + updateDimensions({width: ((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; + $container.find('#layout-size-input').css({ + 'left': gutterPercent * docWidth + }); + // Update the size input value. + $sizeInput.find('input').val(size); + }; + + /** + * + */ + $.fn.drupalLayout = (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]] = ""; + } + }); + } + + /** + * + */ + function prune (options) { + var docWidth = document.documentElement.clientWidth; + var tolerance = (options && 'tolerance' in options && typeof options.tolerance === 'number' && options.tolerance > 0) ? options.tolerance : 0; + return this.each(function () { + var $this = $(this); + var width = parseInt($this.data('layout-width')); + var fits = ((width + (tolerance * 2)) < docWidth); + $this.parent('li').toggleClass('element-hidden', !fits); + }); + } + + /** + * + */ + var methods = { + 'correctEdgeCollisions': correct, + 'prunePreviewChoices': prune + }; + + return function (method) { + // Method calling logic + if (methods[method]) { + return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); + } + 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.find('#layout-previewer-controls').append($breakpointView); + } + }; + + /** + * + */ + function loadDefaultPreview (event) { + event.preventDefault(); + toggleLayoutPreview(event); + updateDimensions({width: document.documentElement.clientWidth}, 250); + } + + /** + * + */ + function loadDevicePreview (event) { + event.preventDefault(); + toggleLayoutPreview(event, 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. + $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); + // Reposition the close button. + $('#layout-preview-close') + .css('left', (leftOffset + size)); + }; + + /** + * Get the total displacement of given region. + * + * @param String region + * Region name. Either "top" or "bottom". + * + * @return + * The total displacement of given region in pixels. + */ + var 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, { + /** + * Returns the preview container element. + */ + layoutContainer: function () { + return '
'; + }, + + /** + * + */ + layoutClose: function () { + return ''; + }, + + /** + * 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. + */ + var keyManager = function (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); + } + } + + /** + * 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. + */ + var mapKeyToChar = function (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; + } + + /** + * 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.install b/core/modules/layout/layout.install new file mode 100644 index 0000000..78d86f2 --- /dev/null +++ b/core/modules/layout/layout.install @@ -0,0 +1,41 @@ + $info) { + array_push($theme_settings_list, 'theme_' . $machine_name . '_settings'); + } + // Remove the 'layout_preview' configuration value from each theme and the + // global theme settings. + foreach ($theme_settings_list as $theme_var) { + $settings = variable_get($theme_var, array()); + if (isset($settings['layout_preview_tab'])) { + unset($settings['layout_preview_tab']); + variable_set($theme_var, $settings); + } + } +} diff --git a/core/modules/layout/layout.module b/core/modules/layout/layout.module index c6ed7ae..50b2b1b 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,5 +87,204 @@ function layout_theme($existing, $type, $theme, $path) { 'template' => $layout['template'], ); } + return $items; } + +/** + * + */ +function layout_preview_toolbar_controls() { + + $links = array(); + + $links += layout_get_devices_list(); + + $links['ruler'] = array( + 'title' => t('Show developer ruler'), + 'href' => '', + 'fragment' => '!', + 'exteranl' => TRUE, + 'attributes' => array( + 'id' => 'layout-preview-ruler-trigger', + 'title' => t('Show the developer ruler above the page preview.'), + ), + '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' => '', + 'fragment' => '!', + 'exteranl' => TRUE, + 'options' => array( + 'fragment' => '!', + 'exteranl' => TRUE, + ), + '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_form_FORM_ID_alter(). + */ +function layout_form_system_theme_settings_alter(&$form, &$form_state) { + // Get the theme for the current settings form. + $theme = (!empty($form_state['build_info']['args'][0])) ? $form_state['build_info']['args'][0] : NULL; + $form['layout_preview'] = array( + '#type' => 'details', + '#title' => t('Device preview'), + '#attributes' => array( + 'class' => array( + 'theme-settings-bottom' + ) + ), + ); + $form['layout_preview']['layout_preview_tab'] = array( + '#type' => 'checkbox', + '#title' => t('Allow theme preview from the toolbar'), + '#default_value' => theme_get_setting('layout_preview_tab', $theme), + '#tree' => FALSE, + ); +} + +function layout_preview_access() { + global $custom_theme, $theme; + if (!empty($custom_theme)) { + $current_theme = $custom_theme; + } + else { + $current_theme = $theme ? $theme : variable_get('theme_default', 'bartik'); + } + return (bool) theme_get_setting('layout_preview_tab', $current_theme); +} + +/** + * Implements hook_toolbar(). + */ +function layout_toolbar() { + + $items['layout_preview'] = array( + '#type' => 'toolbar_item', + 'tab' => array( + 'trigger' => array( + '#theme' => 'html_tag', + '#tag' => 'button', + '#value' => t('Layout preview'), + '#attributes' => array( + 'id' => 'layout-previewer', + 'title' => "Preview page layout", + 'class' => array('icon', 'icon-layout', 'trigger'), + ), + ), + 'device_options' => array( + '#theme' => 'links', + '#links' => layout_preview_toolbar_controls(), + '#attributes' => array( + 'class' => array('layout-previewer-options'), + ), + ), + ), + '#wrapper_attributes' => array( + 'class' => array('layout-preview-toolbar-tab'), + ), + '#attached' => array( + 'library' => array( + array('layout', 'layout.preview'), + ), + ), + '#weight' => 200, + '#access' => layout_preview_access(), + ); + + 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.preview'] = array( + 'title' => 'Preview layouts', + 'website' => 'http://drupal.org/project/layout', + 'version' => VERSION, + 'css' => array( + $path . '/css/layout.preview.base.css', + $path . '/css/layout.preview.theme.css', + ), + 'js' => array( + // Core. + $path . '/js/layout.preview.js' => $options, + array( + 'data' => array( + 'layout' => array( + 'routes' => array( + 'breakpoints' => 'layout/breakpoints' + ), + ), + ), + 'type' => 'setting', + ), + ), + 'dependencies' => array( + array('system', 'jquery'), + array('system', 'drupal.ajax'), + array('system', 'drupalSettings'), + array('system', 'jquery.ui.slider'), + ), + ); + + return $libraries; +} -- 1.7.10.4