diff --git a/core/modules/layout/config/layout.devices.yml b/core/modules/layout/config/layout.devices.yml index da246a4..b39722c 100644 --- a/core/modules/layout/config/layout.devices.yml +++ b/core/modules/layout/config/layout.devices.yml @@ -19,3 +19,8 @@ devices: dimensions: width: 1366 height: 768 + bigscreen: + label: big screen + dimensions: + width: 1600 + height: 800 diff --git a/core/modules/layout/css/layout.base.css b/core/modules/layout/css/layout.base.css deleted file mode 100644 index 475e994..0000000 --- a/core/modules/layout/css/layout.base.css +++ /dev/null @@ -1,167 +0,0 @@ -/** - * 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: 1050; -} -#layout-previewer-container.active { - display: block; - left: 0; -} -.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: 25; -} -#layout-previewer-controls { - position: absolute; - 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; - 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.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/css/layout.theme.css b/core/modules/layout/css/layout.theme.css deleted file mode 100644 index ceb3d90..0000000 --- a/core/modules/layout/css/layout.theme.css +++ /dev/null @@ -1,97 +0,0 @@ -/** - * 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: 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/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# x 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) - .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) { - // 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); - }; - - /** - * Assembles a layout previewer. - */ - var buildPreviewer = function () { - $(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 = $(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; - }); - }; - - /** - * 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; - 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 = 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) { - $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; - 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-previewer-controls').css({ - 'left': gutterPercent * docWidth - }); - // Update the size input value. - $sizeInput.find('input').val(size); - }; - - /** - * - */ - $.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. - $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 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 '
'; - }, - - /** - * 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/js/layout.preview.js b/core/modules/layout/js/layout.preview.js new file mode 100644 index 0000000..9ce0c92 --- /dev/null +++ b/core/modules/layout/js/layout.preview.js @@ -0,0 +1,741 @@ +(function (Drupal, $) { + + Drupal.layout = Drupal.layout || {}; + + 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 $controls; + 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. + var edgeTolerance = 60; + + Drupal.behaviors.layout = { + attach: function (context, settings) { + var $body = $(window.top.document.body).once('layout-preview'); + + if ($body.length) { + // Append the selector to the preview container. + $toolbarTab = $('.layout-preview-toolbar-tab') + .on('click.layout', '#layout-previewer', toggleConfigurationOptions) + .on('mouseleave.layout', '.layout-previewer-options', {open: false}, toggleConfigurationOptions) + .on('click.layout', '.layout-previewer-options .layout-preview-device', {open: false}, toggleConfigurationOptions) + .on('click.layout', '.layout-preview-device', loadDevicePreview) + .on('click.layout', '#layout-preview-ruler-trigger', toggleControls); + // Register a handler on window resize to reposition the tab dropdown. + $(window.top) + .on('resize.layout.tab', handleWindowToolbarResize) + .trigger('resize.layout.tab'); + } + // Remove administrative elements in the document inside the iframe. + if (window.top !== window.self) { + var $frameBody = $(window.self.document.body).once('layout-preview'); + if ($frameBody.length > 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.module b/core/modules/layout/layout.module index a62aeb2..50b2b1b 100644 --- a/core/modules/layout/layout.module +++ b/core/modules/layout/layout.module @@ -95,25 +95,19 @@ function layout_theme($existing, $type, $theme, $path) { * */ 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 = array(); $links += layout_get_devices_list(); $links['ruler'] = array( - 'title' => t('Show responsive ruler'), - 'href' => '', + 'title' => t('Show developer ruler'), + 'href' => '', + 'fragment' => '!', + 'exteranl' => TRUE, 'attributes' => array( - 'title' => t('Show the responsive ruler above the page preview.'), - 'class' => array('additional-options'), + 'id' => 'layout-preview-ruler-trigger', + 'title' => t('Show the developer ruler above the page preview.'), ), 'weight' => 100, ); @@ -134,7 +128,13 @@ function layout_get_devices_list() { foreach($devices as $name => $info) { $links[$name] = array( 'title' => $info['label'], - 'href' => '', + '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'] : '', @@ -212,10 +212,22 @@ function layout_toolbar() { $items['layout_preview'] = array( '#type' => 'toolbar_item', 'tab' => array( - '#type' => 'dropbutton', - '#links' => layout_preview_toolbar_controls(), - '#attributes' => array( - 'class' => array('layout-previewer-options'), + '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( @@ -223,7 +235,7 @@ function layout_toolbar() { ), '#attached' => array( 'library' => array( - array('layout', 'layout.previewer'), + array('layout', 'layout.preview'), ), ), '#weight' => 200, @@ -244,17 +256,17 @@ function layout_library_info() { 'attributes' => array('defer' => TRUE), ); - $libraries['layout.previewer'] = array( + $libraries['layout.preview'] = array( 'title' => 'Preview layouts', 'website' => 'http://drupal.org/project/layout', 'version' => VERSION, 'css' => array( - $path . '/css/layout.base.css', - $path . '/css/layout.theme.css', + $path . '/css/layout.preview.base.css', + $path . '/css/layout.preview.theme.css', ), 'js' => array( // Core. - $path . '/js/layout.js' => $options, + $path . '/js/layout.preview.js' => $options, array( 'data' => array( 'layout' => array(