diff --git a/core/modules/layout/config/layout.devices.yml b/core/modules/layout/config/layout.devices.yml new file mode 100644 index 0000000..da246a4 --- /dev/null +++ b/core/modules/layout/config/layout.devices.yml @@ -0,0 +1,21 @@ +devices: + iphone: + label: iPhone + dimensions: + width: 320 + height: 480 + android: + label: Android + dimensions: + width: 540 + height: 960 + ipad: + label: iPad + dimensions: + width: 768 + height: 1024 + desktop: + label: desktop + dimensions: + width: 1366 + height: 768 diff --git a/core/modules/layout/css/layout.base.css b/core/modules/layout/css/layout.base.css index 316f804..475e994 100644 --- a/core/modules/layout/css/layout.base.css +++ b/core/modules/layout/css/layout.base.css @@ -1,25 +1,90 @@ +/** + * Toolbar tab. + */ +.layout-preview-toolbar-tab { + display: none; +} +.js .toolbar .bar .layout-preview-toolbar-tab.tab { + display: block; + float: right; /* LTR */ +} + #layout-previewer-container { box-shadow: 0 0 10px 0 black; + display: none; height: 100%; left: -200%; position: absolute; width: 100%; - z-index: 1500; + z-index: 1050; } #layout-previewer-container.active { + display: block; left: 0; } -#layout-previewer-container .modal-background { +.layout-modal-background { background-color: black; + background-color: rgba(0,0,0,0.92); bottom: 0; height: 100%; left: 0; - position: absolute; + position: fixed; right: 0; - top: 0; + top: 3em; width: 100%; z-index: 1; } +.layout-slider-background { + background-color: white; + border: 1px solid white; + height: 2em; + position: absolute; + top: 1px; + width: 100%; + z-index: 2; +} +#layout-bp-view { + background-color: rgba(150,150,150,0.6667); + height: 2em; + position: absolute; + top: 2px; + width: 100%; + z-index: 5; +} +.layout-bp { + height: 2em; + position: absolute; + left: auto; + top: 0; + width: 100%; +} +.layout-bp div { + box-sizing: border-box; + height: 2em; + margin: 0 auto; + width: 100%; +} +.layout-bp-min { + background-color: rgba(255,255,255,0.75); + border-color: white; + border-style: solid; + border-width: 0 1px; + position: relative; +} +.layout-bp .label { + box-sizing: border-box; + display: block; + font-size: 0.7em; + height: 1em; + left: -100%; + line-height: 1; + padding-right: 6px; + position: absolute; + text-align: right; + top: 0; + white-space: nowrap; + width: 100%; +} #frame-slider { z-index: 50; } @@ -42,12 +107,15 @@ width: 100%; z-index: 25; } -#layout-size-input { - font-size: 0.75em; +#layout-previewer-controls { position: absolute; - top: 0; + top: 1em; z-index: 100; } +#layout-size-input { + float: left; + font-size: 0.75em; +} #layout-size-input * { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; @@ -63,3 +131,37 @@ } #layout-size-input .unit { } +#layout-devices-list { + display: block; + float: left; +} +/* Toolbar */ +.js .layout-preview-toolbar-tab .dropbutton-wrapper { + min-width: 6em; +} +.js .layout-preview-toolbar-tab .dropbutton-widget { + right: 0; +} +.js .layout-preview-toolbar-tab .dropbutton-widget .trigger { + text-align: right; +} +.js .layout-preview-toolbar-tab .dropbutton-widget .trigger a { + display: inline-block; + text-indent: 9999px; /* LTR */ +} +.layout-preview-toolbar-tab .dropbutton-toggle { + bottom: auto; + height: 3em; +} +.js .layout-preview-toolbar-tab .dropbutton-widget .dropbutton { + overflow: visible; +} + +/* Override Toolbar styling */ +.layout-preview-frame #toolbar-administration { + display: none !important; +} +body.toolbar-tray-open.layout-preview-frame { + margin-left: 0 !important; + margin-right: 0 !important; +} diff --git a/core/modules/layout/css/layout.theme.css b/core/modules/layout/css/layout.theme.css index 3b218d3..ceb3d90 100644 --- a/core/modules/layout/css/layout.theme.css +++ b/core/modules/layout/css/layout.theme.css @@ -1,6 +1,13 @@ +/** + * Toolbar tab. + */ +.layout-preview-toolbar-tab .layout-previewer-options { + background-color: #0f0f0f; +} + #frame-slider { background-attachment: scroll; - background-color: #fcfcfc; + background-color: transparent; background-image: url("../images/ruler.png"); background-position: center bottom; background-repeat: repeat-x; @@ -10,9 +17,9 @@ border-width: 1px 0 0; height: 2em; } -#frame-slider .ui-widget-header { +#frame-slider .ui-slider-range { background: none; - background-color: rgba(60,60,60,0.2); + background-color: rgba(90,90,90,0.2); } #frame-slider.ui-slider-horizontal .ui-slider-handle { background-attachment: scroll; @@ -21,3 +28,70 @@ background-position: center 1px; background-repeat: no-repeat; } + +/** + * Toolbar. + */ +.toolbar .bar .icon.icon-layout { + margin-left: 0; + margin-right: 0; + padding-left: 0; + padding-right: 0; + text-indent: -9999px; + width: 4em; +} +.icon-layout:before { + background-image: url("../images/icon-layout.png"); +} +.toolbar .bar .layout-preview-toolbar-tab .icon-layout:before { + left: 1.75em; +} +.layout-preview-toolbar-tab .open .icon-layout:before, +.layout-preview-toolbar-tab .icon-layout.active:before { + background-image: url("../images/icon-layout-active.png"); +} +.js .layout-preview-toolbar-tab .dropbutton-widget, +.js .layout-preview-toolbar-tab .dropbutton-widget:hover, +.js .layout-preview-toolbar-tab .dropbutton-multiple.open .dropbutton-widget:hover { + background-color: transparent; + border: none; +} +.layout-preview-toolbar-tab .open .dropbutton { + box-shadow: 1em -1em 1em 1em rgba(0, 0, 0, 0.75); +} +.js .layout-preview-toolbar-tab .open .dropbutton li + li { + background-color: white; +} +.js .layout-preview-toolbar-tab .dropbutton a { + color: black; +} +.layout-preview-toolbar-tab .layout-previewer-options { + border-right: none; +} +.layout-preview-toolbar-tab .layout-previewer-options a { + padding-bottom: 1em; + padding-top: 1em; +} +.layout-preview-toolbar-tab .dropbutton-toggle button { + color: #a0a0a0; +} +.layout-preview-toolbar-tab .open .dropbutton-toggle button { + color: black; +} +.layout-preview-toolbar-tab .dropbutton-arrow { + border-width: 0.75em 0.5em 0; +} +.layout-preview-toolbar-tab .dropbutton-multiple.open .dropbutton-arrow { + border-bottom: 0.75em solid; + border-top-color: transparent; +} +.layout-preview-toolbar-tab .dropbutton .secondary-action { + border-top: none; + margin-right: -2em; +} +.layout-preview-toolbar-tab .dropbutton .secondary-action a { + margin-right: 0; +} +.layout-preview-toolbar-tab .dropbutton .additional-options { + border-top: 1px solid #cfcfcf; +} diff --git a/core/modules/layout/images/icon-layout-active.png b/core/modules/layout/images/icon-layout-active.png new file mode 100644 index 0000000..ff89708 --- /dev/null +++ b/core/modules/layout/images/icon-layout-active.png @@ -0,0 +1,14 @@ +‰PNG + + IHDR ú$ pHYs  ÒÝ~ü +OiCCPPhotoshop ICC profilexÚ�SgTSé=÷ÞôBKˆ€”KoR RB‹€‘&*! Jˆ!¡ÙQÁEEÈ ˆŽŽ€ŒQ, Š +Øä!¢Žƒ£ˆŠÊûá{£kÖ¼÷æÍþµ×>ç¬ó�³ÏÀ –H3Q5€ ©BàƒÇÄÆáä.@� +$p³d!sý#ø~<<+"À¾xÓ ÀM›À0‡ÿêB™\€„Àt‘8K€@zŽB¦@F€�˜&S `ËcbãP-`'æÓ€�ø™{[”! ‘ eˆDh;¬ÏVŠEX0fKÄ9Ø-0IWfH°·ÀÎ ² 0Qˆ…){`È##x„™FòW<ñ+®ç*x™²<¹$9E�[-qWW.(ÎI+6aaš@.Ây™2�4àóÌ ‘àƒóýxήÎÎ6޶_-ê¿ÿ"bbãþåÏ«p@át~Ñþ,/³€;€mþ¢%îh^  u÷‹f²@µ éÚWópø~<ß5°j>{‘-¨]cöK'XtÀâ÷ò»oÁÔ(€hƒáÏwÿï?ýG %€fI’q^D$.Tʳ?ÇD �*°AôÁ,ÀÁÜÁ ü`6„B$ÄÂBB +d€r`)¬‚B(†Í°*`/Ô@4ÀQh†“p.ÂU¸=púažÁ(¼� AÈa!ÚˆbŠX#Ž™…ø!ÁH‹$ ɈQ"K‘5H1RŠT UHò=r9‡\Fº‘;È2‚ü†¼G1”�²Q=Ô µC¹¨7„F¢ Ðdt1š� ›Ðr´=Œ6¡çЫhÚ�>CÇ0Àè3Äl0.ÆÃB±8, “c˱"¬ «Æ°V¬»‰õcϱw�EÀ 6wB aAHXLXNØH¨ $4Ú 7 „QÂ'"“¨K´&ºùÄb21‡XH,#Ö�/{ˆCÄ7$‰C2'¹�I±¤TÒÒFÒnR#é,©›4H#“ÉÚdk²9”, +È…ä�äÃä3ää!ò[ +�b@q¤øSâ(RÊjJåå4åe˜2AU£šRݨ¡T5�ZB­¡¶R¯Q‡¨4uš9̓IK¥­¢•Óhh÷i¯ètºÝ•N—ÐWÒËéGè—èôw †ƒÇˆg(›gw¯˜L¦Ó‹ÇT071ë˜ç™™oUX*¶*|‘Ê +•J•&•*/T©ª¦ªÞª UóUËT�©^S}®FU3Sã© Ô–«Uª�PëSSg©;¨‡ªg¨oT?¤~Yý‰YÃLÃOC¤Q ±_ã¼Æ c³x,!k «†u�5Ä&±ÍÙ|v*»˜ý»‹=ª©¡9C3J3W³Ró”f?ã˜qøœtN ç(§—ó~ŠÞï)â)¦4L¹1e\kª–—–X«H«Q«Gë½6®í§�¦½E»Yû�AÇJ'\'Gg�Î�çSÙSݧ +§M=:õ®.ªk¥¡»Dw¿n§î˜ž¾^€žLo§Þy½çú}/ýTýmú§õG X³ $Û Î<Å5qo</ÇÛñQC]Ã@C¥a•a—á„‘¹Ñ<£ÕF�FŒiÆ\ã$ãmÆmÆ£&&!&KMêMîšRM¹¦)¦;L;LÇÍÌÍ¢ÍÖ™5›=1×2ç›ç›×›ß·`ZxZ,¶¨¶¸eI²äZ¦Yî¶¼n…Z9Y¥XUZ]³F­�­%Ö»­»§§¹N“N«žÖgðñ¶É¶©·°åØÛ®¶m¶}agbg·Å®Ã“}º}�ý= ‡Ù«Z~s´r:V:ޚΜî?}Åô–é/gXÏÏØ3ã¶Ë)Äi�S›ÓGgg¹sƒóˆ‹‰K‚Ë.—>.›ÆÝȽäJtõq]ázÒõ�›³›Âí¨Û¯î6îiî‡ÜŸÌ4Ÿ)žY3sÐÃÈCàQåÑ? Ÿ•0k߬~OCO�gµç#/c/‘W­×°·¥wª÷aï>ö>rŸã>ã<7Þ2ÞY_Ì7À·È·ËOÃož_…ßC#ÿdÿzÿѧ€%g‰�A�[ûøz|!¿Ž?:Ûeö²ÙíAŒ ¹AA�‚­‚åÁ­!hÈì�­!÷ç˜Î‘Îi…P~èÖÐaæa‹Ã~ '…‡…W†?ŽpˆXÑ1—5wÑÜCsßDúD–DÞ›g1O9¯-J5*>ª.j<Ú7º4º?Æ.fYÌÕX�XIlK9.*®6nl¾ßüíó‡â�â ã{˜/È]py¡ÎÂô…§©.,:–@LˆN8”ðA*¨Œ%òw%Ž +yÂÂg"/Ñ6шØC\*NòH*Mz’쑼5y$Å3¥,幄'©�¼L LÝ›:žšv m2=:½1ƒ’‘�qBª!M“¶gêgæfvˬe…²þÅn‹·/•Ék³�¬Y- +¶B¦èTZ(×*²geWf¿Í‰Ê9–«ž+Íí̳ÊÛ�7œïŸÿíÂá’¶¥†KW-X潬j9²‰Š®Û—Ø(Üxå‡oÊ¿™Ü”´©«Ä¹dÏfÒféæÞ-ž[–ª—æ—n ÙÚ´ ßV´íõöEÛ/—Í(Û»ƒ¶C¹£¿<¸¼e§ÉÎÍ;?T¤TôTúT6îÒݵa×ønÑî{¼ö4ìÕÛ[¼÷ý>ɾÛUUMÕfÕeûIû³÷?®‰ªéø–ûm]­NmqíÇÒý#¶×¹ÔÕÒ=TR�Ö+ëGǾþ�ïw- 6 U�œÆâ#pDyäé÷ ß÷ :ÚvŒ{¬áÓvg/jBšòšF›Sšû[b[ºOÌ>ÑÖêÞzüGÛœ499â?rýéü§CÏdÏ&žþ¢þË®/~øÕë×Îјѡ—ò—“¿m|¥ýêÀë¯ÛÆÂƾÉx31^ôVûíÁwÜwï£ßOä| (ÿhù±õSЧû“““ÿ˜óüc3-Û cHRMz%€ƒùÿ€éu0ê`:˜o’_ÅFkIDATxÚ쓱 €0 ß$‹1«š‘(Rdû) 4 7)'Y.ü/ë-$•1TÜ�’ÉÓR’7±™²»wohÚkSM;ÐôÅLÿ!Fg’Zkø '’x¨�ä|öÛ<”©!¥°<ë>SîË댞 |IEND®B`‚ \ 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é=÷ÞôBKˆ€”KoR RB‹€‘&*! Jˆ!¡ÙQÁEEÈ ˆŽŽ€ŒQ, Š +Øä!¢Žƒ£ˆŠÊûá{£kÖ¼÷æÍþµ×>ç¬ó�³ÏÀ –H3Q5€ ©BàƒÇÄÆáä.@� +$p³d!sý#ø~<<+"À¾xÓ ÀM›À0‡ÿêB™\€„Àt‘8K€@zŽB¦@F€�˜&S `ËcbãP-`'æÓ€�ø™{[”! ‘ eˆDh;¬ÏVŠEX0fKÄ9Ø-0IWfH°·ÀÎ ² 0Qˆ…){`È##x„™FòW<ñ+®ç*x™²<¹$9E�[-qWW.(ÎI+6aaš@.Ây™2�4àóÌ ‘àƒóýxήÎÎ6޶_-ê¿ÿ"bbãþåÏ«p@át~Ñþ,/³€;€mþ¢%îh^  u÷‹f²@µ éÚWópø~<ß5°j>{‘-¨]cöK'XtÀâ÷ò»oÁÔ(€hƒáÏwÿï?ýG %€fI’q^D$.Tʳ?ÇD �*°AôÁ,ÀÁÜÁ ü`6„B$ÄÂBB +d€r`)¬‚B(†Í°*`/Ô@4ÀQh†“p.ÂU¸=púažÁ(¼� AÈa!ÚˆbŠX#Ž™…ø!ÁH‹$ ɈQ"K‘5H1RŠT UHò=r9‡\Fº‘;È2‚ü†¼G1”�²Q=Ô µC¹¨7„F¢ Ðdt1š� ›Ðr´=Œ6¡çЫhÚ�>CÇ0Àè3Äl0.ÆÃB±8, “c˱"¬ «Æ°V¬»‰õcϱw�EÀ 6wB aAHXLXNØH¨ $4Ú 7 „QÂ'"“¨K´&ºùÄb21‡XH,#Ö�/{ˆCÄ7$‰C2'¹�I±¤TÒÒFÒnR#é,©›4H#“ÉÚdk²9”, +È…ä�äÃä3ää!ò[ +�b@q¤øSâ(RÊjJåå4åe˜2AU£šRݨ¡T5�ZB­¡¶R¯Q‡¨4uš9̓IK¥­¢•Óhh÷i¯ètºÝ•N—ÐWÒËéGè—èôw †ƒÇˆg(›gw¯˜L¦Ó‹ÇT071ë˜ç™™oUX*¶*|‘Ê +•J•&•*/T©ª¦ªÞª UóUËT�©^S}®FU3Sã© Ô–«Uª�PëSSg©;¨‡ªg¨oT?¤~Yý‰YÃLÃOC¤Q ±_ã¼Æ c³x,!k «†u�5Ä&±ÍÙ|v*»˜ý»‹=ª©¡9C3J3W³Ró”f?ã˜qøœtN ç(§—ó~ŠÞï)â)¦4L¹1e\kª–—–X«H«Q«Gë½6®í§�¦½E»Yû�AÇJ'\'Gg�Î�çSÙSݧ +§M=:õ®.ªk¥¡»Dw¿n§î˜ž¾^€žLo§Þy½çú}/ýTýmú§õG X³ $Û Î<Å5qo</ÇÛñQC]Ã@C¥a•a—á„‘¹Ñ<£ÕF�FŒiÆ\ã$ãmÆmÆ£&&!&KMêMîšRM¹¦)¦;L;LÇÍÌÍ¢ÍÖ™5›=1×2ç›ç›×›ß·`ZxZ,¶¨¶¸eI²äZ¦Yî¶¼n…Z9Y¥XUZ]³F­�­%Ö»­»§§¹N“N«žÖgðñ¶É¶©·°åØÛ®¶m¶}agbg·Å®Ã“}º}�ý= ‡Ù«Z~s´r:V:ޚΜî?}Åô–é/gXÏÏØ3ã¶Ë)Äi�S›ÓGgg¹sƒóˆ‹‰K‚Ë.—>.›ÆÝȽäJtõq]ázÒõ�›³›Âí¨Û¯î6îiî‡ÜŸÌ4Ÿ)žY3sÐÃÈCàQåÑ? Ÿ•0k߬~OCO�gµç#/c/‘W­×°·¥wª÷aï>ö>rŸã>ã<7Þ2ÞY_Ì7À·È·ËOÃož_…ßC#ÿdÿzÿѧ€%g‰�A�[ûøz|!¿Ž?:Ûeö²ÙíAŒ ¹AA�‚­‚åÁ­!hÈì�­!÷ç˜Î‘Îi…P~èÖÐaæa‹Ã~ '…‡…W†?ŽpˆXÑ1—5wÑÜCsßDúD–DÞ›g1O9¯-J5*>ª.j<Ú7º4º?Æ.fYÌÕX�XIlK9.*®6nl¾ßüíó‡â�â ã{˜/È]py¡ÎÂô…§©.,:–@LˆN8”ðA*¨Œ%òw%Ž +yÂÂg"/Ñ6шØC\*NòH*Mz’쑼5y$Å3¥,幄'©�¼L LÝ›:žšv m2=:½1ƒ’‘�qBª!M“¶gêgæfvˬe…²þÅn‹·/•Ék³�¬Y- +¶B¦èTZ(×*²geWf¿Í‰Ê9–«ž+Íí̳ÊÛ�7œïŸÿíÂá’¶¥†KW-X潬j9²‰Š®Û—Ø(Üxå‡oÊ¿™Ü”´©«Ä¹dÏfÒféæÞ-ž[–ª—æ—n ÙÚ´ ßV´íõöEÛ/—Í(Û»ƒ¶C¹£¿<¸¼e§ÉÎÍ;?T¤TôTúT6îÒݵa×ønÑî{¼ö4ìÕÛ[¼÷ý>ɾÛUUMÕfÕeûIû³÷?®‰ªéø–ûm]­NmqíÇÒý#¶×¹ÔÕÒ=TR�Ö+ëGǾþ�ïw- 6 U�œÆâ#pDyäé÷ ß÷ :ÚvŒ{¬áÓvg/jBšòšF›Sšû[b[ºOÌ>ÑÖêÞzüGÛœ499â?rýéü§CÏdÏ&žþ¢þË®/~øÕë×Îјѡ—ò—“¿m|¥ýêÀë¯ÛÆÂƾÉx31^ôVûíÁwÜwï£ßOä| (ÿhù±õSЧû“““ÿ˜óüc3-Û cHRMz%€ƒùÿ€éu0ê`:˜o’_ÅF�IDATxÚì”Á à ‡ˆ>RK*p ”B%€è ½1 -1]-ˆˆ”PV)th7âÎ!Jh+ÜNÕWËçN}žÎ¿£•S·Â¸¿ó¦”8V‹WÊõ;ÿ˜Òú5´ÈÆ{Ò¡Í›yžÃ!îh–¾©6ò‰ý‘�9ÁGṁÇ3ë`^ŽùŒ‡nÐ7êчìNZOó!}d9ÿ¹>§öÊ}¿ü -0Äé}ï_ˆtQIEND®B`‚ \ No newline at end of file + IHDRZ )#yMtEXtSoftwareAdobe ImageReadyqÉe<CIDATxÚì×± AuXW°�XÜ×TB®ª¥þNf’ž€F´š _î¸ít€hÐýC;"¼à]†û VÁïö�IEND®B`‚ \ No newline at end of file diff --git a/core/modules/layout/js/layout.js b/core/modules/layout/js/layout.js index e246d76..e165d60 100644 --- a/core/modules/layout/js/layout.js +++ b/core/modules/layout/js/layout.js @@ -2,30 +2,37 @@ Drupal.layout = Drupal.layout || {}; - var size; - var handles = []; - var leftOffset; - var $frame; - var $slider; - var $container; - var $sizeInput; - var active = false; + var $toolbarTab = $(); + var size; // The width of the preview container. + var handles = []; // The values of the jQuery UI Slider handles. + var leftOffset; // The left value of the iframe containing the previewed page. + var $frame; // The iframe that contains the previewed page. + var iframeDocument; // The document of the iframe that contains the preview. + var $slider; // The jQuery UI Slider widget that adjusts the iframed preview. + var $container; // The container of the page preview component. + var $sizeInput; // The input element that display the width of the preview. + var breakpoints = {}; // A list of breakpoints, keyed by configuration string. + var $breakpointView; // The container of the breakpoint views. Drupal.behaviors.layout = { attach: function (context, settings) { var $body = $(window.top.document.body).once('layout-preview'); if ($body.length) { - // Set up the trigger link. - $('#toolbar-tab-layout_preview').on('click.layout', Drupal.layout.toggleLayoutPreview); + // Append the selector to the preview container. + $toolbarTab = $('.layout-preview-toolbar-tab') + .on('click.layout', '#layout-previewer', loadDefaultPreview) + .on('click.layout', '.layout-preview-device', loadDevicePreview); + // Register a handler on window resize to reposition the tab dropdown. + $(window.top) + .on('resize.layout.tab', handleWindowToolbarResize); } // Remove administrative elements in the document inside the iframe. if (window.top !== window.self) { - $(context) - .find('body') - .removeClass('toolbar-tray-open') - .find('#toolbar-administration') - .remove(); + var $frameBody = $(window.self.document.body).once('layout-preview'); + if ($frameBody.length > 0) { + $frameBody.get(0).className += ' layout-preview-frame'; + } } } }; @@ -33,22 +40,40 @@ /** * */ - Drupal.layout.toggleLayoutPreview = function (event) { + var toggleConfigurationOptions = function (event) { + event.preventDefault(); + var open = (event.data && typeof event.data.open === 'boolean') ? event.data.open : undefined; + $(event.delegateTarget) + .find('.layout-previewer-options') + .toggle(open) + .correctEdgeCollisions(); + } + + /** + * Toggles the layout preview component on or off. + * + * When first toggled on, the layout preview component is built. All + * subsequent toggles hide or show the build component. + * + * @param Object Event + * - jQuery Event object. + */ + var toggleLayoutPreview = function (activate) { // Build the previewer if it doesn't exist. if (!$container) { - // Size is the width of the iframe. - size = size || window.top.document.documentElement.clientWidth; // Initialize the handle positions. handles = (handles.length) ? handles : [0, document.documentElement.clientWidth]; - Drupal.layout.buildPreviewer(); + buildPreviewer(); + // Size is the width of the iframe. + updateDimensions({width: (size || window.top.document.documentElement.clientWidth)}); } - $container.toggleClass('active'); + $container.toggleClass('active', activate); }; /** - * + * Assembles a layout previewer. */ - Drupal.layout.buildPreviewer = function (width) { + var buildPreviewer = function () { $(window.top.document.body).once('layout-preview-container', function (index, element) { $container = $(Drupal.theme('layoutContainer')) .appendTo(window.top.document.body); @@ -57,51 +82,65 @@ 'width': size }) .appendTo($container); + // Slider. + $slider = $(Drupal.theme('layoutSlider')) + .slider({ + 'animate': 'fast', + 'range': true, + 'max': document.documentElement.clientWidth, + 'min': 0, + 'values': handles, + 'slide': handleSlide + }) + .prependTo($container); + + // Load the breakpoints for the current theme. + if ('breakpoints' in Drupal.settings.layout.routes) { + $.ajax(Drupal.settings.layout.routes.breakpoints) + .success(breakpointsCallback); + } + // Attach controls + $container.append(Drupal.theme('layoutControls')); + // Width label. + $sizeInput = $(Drupal.theme('layoutSizeInput')) + .appendTo($container.find('#layout-previewer-controls')); + // Displace the top of the container. + $container + .css({ + top: getDisplacement('top'), + }) + .attr('data-offset-top', getDisplacement('top')); + // The contentDocument property is not supported in IE until IE8. + iframeDocument = $frame[0].contentDocument || $frame[0].contentWindow.document; + + $(window.top).on('resize.layout', handleWindowResize); + $sizeInput.on('keypress.layout', {pattern: /^[0-9\.]$/, callback: handleSizeInputChange}, keyManager); + $container.on('sizeUpdate.layout', refreshPreviewSizing); + + // Trigger a resize to kick off some initial placements. + $(window.top).triggerHandler('resize.layout'); + + // Load the current page URI into the preview iframe. + // @todo, are there any security implications to loading a page like this? + iframeDocument.location.href = Drupal.settings.basePath + Drupal.settings.currentPath; }); - // Slider. - $slider = $('
') - .slider({ - 'animate': 'fast', - 'range': true, - 'max': document.documentElement.clientWidth, - 'min': 0, - 'values': handles, - 'slide': Drupal.layout.handleSlide - }) - .prependTo($container); - // Width label. - $sizeInput = $(Drupal.theme('layoutSizeInput')) - .appendTo($container); - // Displace the top of the container. - $container - .css({ - top: Drupal.layout.getDisplacement('top'), - }) - .attr('data-offset-top', Drupal.overlay.getDisplacement('top')); - // The contentDocument property is not supported in IE until IE8. - var iframeDocument = $frame[0].contentDocument || $frame[0].contentWindow.document; - - $(window.top).on('resize.layout', Drupal.layout.handleWindowResize); - $sizeInput.on('keypress.layout', {pattern: /^[0-9\.]$/, callback: Drupal.layout.handleSizeInputChange}, keyManager); - $container.on('sizeUpdate.layout', refreshPreviewSizing); - - // Trigger a resize to kick off some initial placements. - $(window.top).triggerHandler('resize.layout'); - - // Load the current page in the iframe. - // @todo, the location.href shouldn't be passed in naked like this. - iframeDocument.location.href = document.location.href; }; /** + * Responds to a jQuery UI Slider slide event. * + * @param Object Event + * - jQuery Event object. + * + * @param Object ui + * - jQuery Slider widget state information resulting from a slide event. */ - Drupal.layout.handleSlide = function (event, ui) { + var handleSlide = function (event, ui) { // Layout will control the placement of the handles. event.preventDefault(); var delta = 0; var vals = []; - var handle, split; + var handle, split, width; // Get the delta of the original value of the handles and the new value. for (var i = ui.values.length - 1; i >= 0; i--) { // Get the original handle value. @@ -116,8 +155,10 @@ // The value of the other handle is the inverse of the percentage of the // active handle. vals[otherHandle] = max * (1 - (value / max)); - // Update the dimensions variables in the closure. - updateDimensions(vals, max); + // Get the updated width of the viewport. + width = Math.abs(vals[i] - vals[otherHandle]); + // Update the dimension variables in the closure. + updateDimensions({width: width}); // Only one handle moves at a time, so if a handle-move was processed, // then break; break; @@ -126,9 +167,12 @@ }; /** + * Responds to keypress events from the frame size input. * + * @param Object Event + * - jQuery Event object. */ - Drupal.layout.handleSizeInputChange = function (event) { + var handleSizeInputChange = function (event) { var newSize; if (event.isDefaultPrevented()) { return false; @@ -145,25 +189,41 @@ if (event.keyCode === 13) { var newSize = parseFloat($sizeInput.find('input').val()); if (newSize > 0) { - var max = $slider.slider('option', 'max'); - var vals = []; - var gutterPercent = (1 - (newSize / max)) / 2; - vals[0] = gutterPercent * max; - vals[1] = (gutterPercent * max) + newSize; // Update the dimensions variables in the closure. - updateDimensions(vals, max, 250); + updateDimensions({width: newSize}, 250); } } }; /** + * Updates the dimension variables of the previewer components. * + * @param Array values + * - An array that contains the position values of the handles of the jQuery + * UI slider. + * + * @param max + * - The maximum width of the previewer. Often this is just the width of the + * client. + * + * @param speed + * - A number representing time in milliseconds or a jQuery speed keyword. + * Determines the speed at which animations between changes dimension values + * should take place. Defaults to zero. */ - var updateDimensions = function (values, max, speed) { + var updateDimensions = function (dimensions, speed) { + var width = dimensions.width || NaN; + var height = dimensions.height || NaN; + // Calculate the handle placements. + var max = $slider.slider('option', 'max'); + var values = []; + var gutterPercent = (1 - (width / max)) / 2; + values[0] = gutterPercent * max; + values[1] = (gutterPercent * max) + width; // Store the new values of the handles. handles = [values[0], values[1]]; // Set the new size of the frame. - size = max * ((values[1] - values[0]) / max); + size = Math.round((max * ((values[1] - values[0]) / max)) * 10) / 10; // Set the left offset of the frame. leftOffset = max * (values[0] / max); // Trigger a dimension change. @@ -173,7 +233,19 @@ /** * */ - Drupal.layout.handleWindowResize = function (event) { + var handleWindowToolbarResize = function (event) { + $toolbarTab + .find('.layout-previewer-options') + .correctEdgeCollisions(); + }; + + /** + * Responds to window resize events. + * + * @param Object Event + * - jQuery Event object. + */ + var handleWindowResize = function (event) { var doc = this.document.documentElement; var docWidth = doc.clientWidth; var framePercent = size / docWidth; @@ -186,7 +258,7 @@ .slider('values', [(gutterPercent * docWidth), ((gutterPercent + framePercent) * docWidth)]); // If the window has been reduced below the width of the frame, reduce the // width of the frame. - size = (docWidth < size) ? docWidth : size; + updateDimensions({width: ((docWidth < size) ? docWidth : size)}); // Adjust the parameters of the frame. $frame.css({ 'left': gutterPercent * docWidth, @@ -195,7 +267,7 @@ // Adjust the position of the size input. var inputPercent = $sizeInput.width() / docWidth; gutterPercent = (1 - inputPercent) / 2; - $sizeInput.css({ + $container.find('#layout-previewer-controls').css({ 'left': gutterPercent * docWidth }); // Update the size input value. @@ -205,6 +277,157 @@ /** * */ + $.fn.correctEdgeCollisions = (function () { + + function correct () { + // Clear any previous corrections. + clear.apply(this); + // Go through each element and correct edge collisions. + return this.each(function (index, element) { + var $this = $(this); + var width = $this.width(); + var height = $this.height(); + var clientW = document.documentElement.clientWidth; + var clientH = document.documentElement.clientHeight; + var collisions = { + 'top': null, + 'right': null, + 'bottom': null, + 'left': null + }; + // Determine if the element is too big for the document. Resize to fit. + if (width > clientW) { + $this.width(clientW); + // If the element is too wide, it will collide on both left and right. + collisions.left = true; + collisions.right = true; + } + if (height > clientH) { + $this.height(clientH); + // If the element is too high, it will collide on both top and bottom. + collisions.top = true; + collisions.bottom = true; + } + // Check each edge for a collision. + if (!collisions.top && $this.offset().top < 0) { + collisions.top = true; + } + if (!collisions.right && (($this.offset().left + width) > clientW)) { + collisions.right = true; + } + if (!collisions.bottom && (($this.offset().top + height) > clientH)) { + collisions.bottom = true; + } + if (!collisions.left && $this.offset().left < 0) { + collisions.left = true; + } + // Set the offset to zero for any collision on an edge. + for (var edge in collisions) { + if (collisions.hasOwnProperty(edge)) { + if (collisions[edge]) { + $this.css(edge, 0); + } + } + } + }); + } + + function clear () { + var edges = ['top', 'right', 'bottom', 'left']; + return this.each(function (index, element) { + for (var i = 0; i < edges.length; i++) { + this.style[edges[i]] = ""; + } + }); + } + + + var methods = { + 'correct': correct, + 'clear': clear + }; + + return function (method) { + // Method calling logic + if (methods[method]) { + return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); + } else if (typeof method === 'object' || ! method) { + return methods.correct.apply(this, arguments); + } else { + $.error(Drupal.t('Method @method does not exist in this plugin.', {'@method': method})); + } + }; + + }()); + + /** + * Renders breakpoint configuration to an HTML view. + * + * @param Object data + * - Breakpoint configuration data. The keys of the object correspond to the + * keys of theme-configured breakpoints. The value of each key is a string + * that represents a media query. + * + * @param String textStatus + * - The status of the AJAX request. + * + * @param Object jqXHR + * - A jQuery XMLHttpRequest object. + */ + var breakpointsCallback = function (data, textStatus, jqXHR) { + $breakpointView = $(Drupal.theme('layoutBreakpointView')); + var $item, options; + for (var bp in data) { + if (data.hasOwnProperty(bp)) { + breakpoints[bp] = parseMediaQuery(data[bp]); + // Append a representation of each breakpoint to the frame slider. + if (breakpoints[bp].hasquery) { + options = breakpoints[bp]; + options.id = 'layout-bp-' + bp.replace(/\./g, '-'); + options.label = bp; + $item = $(Drupal.theme('layoutBreakpointItemView', options)); + $breakpointView.prepend($item); + } + } + } + if ($breakpointView.children().length) { + // @todo, These breakpoints should be sorted by minw eventually. For now + // we assume they are listed from small to largest. + $container.append($breakpointView); + } + }; + + /** + * + */ + function loadDefaultPreview (event) { + event.preventDefault(); + toggleLayoutPreview(); + updateDimensions({width: document.documentElement.clientWidth}, 250); + } + + /** + * + */ + function loadDevicePreview (event) { + event.preventDefault(); + toggleLayoutPreview(true); + var $link = $(event.target); + var width = $link.data('layout-width') + updateDimensions({width: width}, 250); + } + + /** + * Redraws the layout preview component based on the stored dimensions. + * + * @param Object event + * - A jQuery event object. + * + * @param Number/String speed + * - A number representing time in milliseconds or a jQuery speed keyword. + * Determines the speed at which animations between changes dimension values + * should take place. Defaults to zero. + */ var refreshPreviewSizing = function (event, speed) { speed = speed || 0; // Adjust the frame. @@ -220,13 +443,13 @@ /** * Get the total displacement of given region. * - * @param region + * @param String region * Region name. Either "top" or "bottom". * * @return * The total displacement of given region in pixels. */ - Drupal.layout.getDisplacement = function (region) { + var getDisplacement = function (region) { var displacement = 0; var lastDisplaced = $('[data-offset-' + region + ']'); if (lastDisplaced.length) { @@ -237,31 +460,83 @@ $.extend(Drupal.theme, { /** - * Theme function to create the overlay iframe element. + * Returns the preview container element. */ layoutContainer: function () { - return '
'; + return '
'; }, /** - * Theme function to create an overlay iframe element. + * Returns an overlay iframe element. */ layoutFrame: function (url) { return ''; }, /** + * Returns the HTML for the jQuery UI Slider attachment. + */ + layoutSlider: function () { + return '
'; + }, + + /** * */ + layoutControls: function () { + return '
'; + }, + + /** + * Returns the input element for changing the preview width. + */ layoutSizeInput: function () { return '
' + Drupal.t('Width') + 'px
'; + }, + + /** + * Returns the wrapper for breakpoint item views. + */ + layoutBreakpointView: function () { + return '
' + }, + + /** + * Returns individual breakpoint configuration views. + */ + layoutBreakpointItemView: function (options) { + var markup = ''; + options = options || {}; + + markup += '
'; + markup += '
'; + markup += '
' + options.label + '
'; + markup += '
'; + markup += '
'; + + return markup; } }); /** + * Handles key input. * + * Fires a callback function with either return the key that was pressed in + * the event.key property, or, if the key is a control key or does not match + * a supplied pattern, then null as the event.key value. + * + * @param Regex event.data.pattern + * - A regular expression that filters allowed key input. Only keys matching + * the expression will be returned. All other keys return null. + * + * @param Function event.data.callback + * - A callback function to be invoked after a key is processed. Any + * variadic parameters supplied to keyManager are passed through as well. + * + * @param Array event.data.controls + * - An Array of char codes that should be ignored as control keys. */ - function keyManager (event) { + var keyManager = function (event) { event.data = event.data || {}; var pattern = event.data.pattern || undefined; var callback = event.data.callback || undefined; @@ -292,9 +567,20 @@ } /** + * Maps keyCode to Strings, taking the shift key state into account. * + * @param Boolean isShifted + * - A Boolean representing if the shift key was pressed (true) or not + * (false) + * + * @param Number keyCode + * - The numeric code of the key that was pressed. + * + * @return String + * - A single character corresponding to the keyCode or null if no + * correspondence is found. */ - function mapKeyToChar(isShifted, keyCode) { + var mapKeyToChar = function (isShifted, keyCode) { if (keyCode === 27 || keyCode === 8 || keyCode === 9 @@ -352,4 +638,19 @@ } return character; } + + /** + * Parses a String representing a media query into usable values. + * + * @param String mq + * - A String representing a media query e.g. 'all and (min-width: 800px)' + */ + var parseMediaQuery = function (mq) { + return { + media : mq.split("(")[0].match(/(only\s+)?([a-zA-Z]+)\s?/) && RegExp.$2 || "all", + hasquery: mq.indexOf("(") > -1, + minw : mq.match(/\(min\-width:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/) && parseFloat(RegExp.$1) + (RegExp.$2 || ""), + maxw : mq.match(/\(max\-width:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/) && parseFloat(RegExp.$1) + (RegExp.$2 || "") + } + } }(Drupal, jQuery)); diff --git a/core/modules/layout/layout.module b/core/modules/layout/layout.module index 321face..6cc9288 100644 --- a/core/modules/layout/layout.module +++ b/core/modules/layout/layout.module @@ -5,6 +5,8 @@ * Manages page layouts for content presentation. */ +use Symfony\Component\HttpFoundation\JsonResponse; + /** * Implements hook_menu(). */ @@ -25,6 +27,13 @@ function layout_menu() { 'access arguments' => array(4), 'file' => 'layout.admin.inc', ); + // Fetch breakpoints for a theme. + $items['layout/breakpoints'] = array( + 'page callback' => 'layout_retrieve_theme_breakpoints_jsonp', + 'access callback' => 'user_access', + 'access arguments' => array('administer layouts'), + 'type' => MENU_CALLBACK, + ); return $items; } @@ -78,33 +87,112 @@ function layout_theme($existing, $type, $theme, $path) { 'template' => $layout['template'], ); } + return $items; } /** + * + */ +function layout_preview_toolbar_controls() { + $links['trigger'] = array( + 'title' => t('Layout preview'), + 'href' => '', + 'attributes' => array( + 'id' => 'layout-previewer', + 'title' => "Preview page layout", + 'class' => array('icon', 'icon-layout'), + ), + 'weight' => 0, + ); + + $links += layout_get_devices_list(); + + $links['ruler'] = array( + 'title' => t('Show responsive ruler'), + 'href' => '', + 'attributes' => array( + 'title' => t('Show the responsive ruler above the page preview.'), + 'class' => array('additional-options'), + ), + 'weight' => 100, + ); + + return $links; +} + +/** + * Page callback: Returns the breakpoints of the current active theme. + * + * @see layout_menu(). + */ +function layout_get_devices_list() { + $devices = config('layout.devices')->get('devices'); + + $links = array(); + + foreach($devices as $name => $info) { + $links[$name] = array( + 'title' => $info['label'], + 'href' => '', + 'attributes' => array( + 'class' => array('layout-preview-device'), + 'data-layout-width' => ($info['dimensions']['width']) ? $info['dimensions']['width'] : '', + 'data-layout-height' => ($info['dimensions']['height']) ? $info['dimensions']['height'] : '', + ), + ); + } + return $links; +} + +/** + * Page callback: Returns the breakpoints of the current active theme. + * + * @see layout_menu(). + */ +function layout_retrieve_theme_breakpoints_jsonp() { + global $theme_key; + + // Get the configured breakpoint to switch from vertical to horizontal + // toolbar presentation. + $media_queries = array(); + $breakpoints = entity_load('breakpoint_group', 'theme.' . $theme_key . '.' . $theme_key); + if (!empty($breakpoints)) { + $media_queries = array_map( + function($object) { + return $object->mediaQuery; + }, + $breakpoints->breakpoints + ); + } + + $response = new JsonResponse($media_queries); + return $response; +} + +/** * Implements hook_toolbar(). */ function layout_toolbar() { - $items = array(); $items['layout_preview'] = array( + '#type' => 'toolbar_item', 'tab' => array( - 'title' => t('Layout preview'), - 'href' => '', - 'html' => FALSE, - 'attributes' => array( - 'title' => t('Preview page layout'), - 'id' => 'layout-previewer', + '#type' => 'dropbutton', + '#links' => layout_preview_toolbar_controls(), + '#attributes' => array( + 'class' => array('layout-previewer-options'), ), ), - 'tray' => array( - '#attached' => array( - 'library' => array( - array('layout', 'layout.previewer'), - ), + '#wrapper_attributes' => array( + 'class' => array('layout-preview-toolbar-tab'), + ), + '#attached' => array( + 'library' => array( + array('layout', 'layout.previewer'), ), ), - 'weight' => 200, + '#weight' => 200, ); return $items; @@ -132,6 +220,16 @@ function layout_library_info() { 'js' => array( // Core. $path . '/js/layout.js' => $options, + array( + 'data' => array( + 'layout' => array( + 'routes' => array( + 'breakpoints' => 'layout/breakpoints' + ), + ), + ), + 'type' => 'setting', + ), ), 'dependencies' => array( array('system', 'jquery'),