diff --git a/core/modules/layout/config/display.bound.admin_master.yml b/core/modules/layout/config/display.bound.admin_master.yml new file mode 100644 index 0000000..5b33c5f --- /dev/null +++ b/core/modules/layout/config/display.bound.admin_master.yml @@ -0,0 +1,41 @@ +id: admin_master +label: Default admin layout +layout: static_layout:layout__two-col +layoutSettings: { } +blockInfo: + block.ikwqqmzh: + region: first + weight: '100' + region-type: content + block.pasmpbio: + region: first + weight: '200' + region-type: content + block.hiuobsyq: + region: first + weight: '300' + region-type: content + block.aduoefgo: + region: second + weight: '100' + region-type: aside + block.cefddxxz: + region: second + weight: '200' + region-type: aside + block.fvurzags: + region: second + weight: '300' + region-type: aside + block.cwycrmwm: + region: second + weight: '400' + region-type: aside + block.nrinfrbu: + region: second + weight: '500' + region-type: aside + block.hhqpbxal: + region: second + weight: '600' + region-type: aside diff --git a/core/modules/layout/config/display.bound.front_master.yml b/core/modules/layout/config/display.bound.front_master.yml new file mode 100644 index 0000000..fe9c371 --- /dev/null +++ b/core/modules/layout/config/display.bound.front_master.yml @@ -0,0 +1,37 @@ +id: front_master +label: Default layout +layout: static_layout:layout__two-col +layoutSettings: { } +blockInfo: + block.nnadgxph: + region: first + weight: '100' + region-type: content + block.xytwvpdi: + region: first + weight: '200' + region-type: content + block.xzcwrtdy: + region: first + weight: '300' + region-type: content + block.miqumptd: + region: first + weight: '400' + region-type: content + block.htgrsglh: + region: second + weight: '100' + region-type: aside + block.scxwxgon: + region: second + weight: '200' + region-type: aside + block.fqcxgrof: + region: second + weight: '300' + region-type: aside + block.ebrcmkge: + region: second + weight: '400' + region-type: aside diff --git a/core/modules/layout/css/layout.base-rtl.css b/core/modules/layout/css/layout.base-rtl.css new file mode 100644 index 0000000..e0e3a00 --- /dev/null +++ b/core/modules/layout/css/layout.base-rtl.css @@ -0,0 +1,3 @@ +/** + * @file layout.base-rtl.css + */ diff --git a/core/modules/layout/css/layout.base.css b/core/modules/layout/css/layout.base.css new file mode 100755 index 0000000..323f22e --- /dev/null +++ b/core/modules/layout/css/layout.base.css @@ -0,0 +1,59 @@ +/** + * @file layout.base.css + */ + +.layout-app, +.layout-app * { + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +.layout-app .form-textarea { + display: none; +} + +/** + * Regions. + */ +.layout-app header .operations { + float: left; /* LTR */ +} +.layout-app header .info { + float: right; /* LTR */ +} + +/** + * Blocks. + */ +.layout-app .blocks { + display: table; + table-layout: fixed; + width: 100%; +} +.layout-app .blocks .row { + display: table-row; +} +.layout-app .blocks .block { + display: table-cell; + overflow: hidden; +} +.layout-app .blocks .block:hover { + cursor: pointer; +} +.layout-app .block > .lining { + display: inline-block; + height:100%; + position: relative; + width: 100%; +} + +/** + * Block information. + */ +.layout-app .block .type-indicator, +.layout-app .block .label { + display: block; +} +.layout-app .block .type-indicator { + float: left; /* LTR */ +} diff --git a/core/modules/layout/css/layout.icons-rtl.css b/core/modules/layout/css/layout.icons-rtl.css new file mode 100644 index 0000000..aed81b5 --- /dev/null +++ b/core/modules/layout/css/layout.icons-rtl.css @@ -0,0 +1,3 @@ +/** + * @file layout.icons-rtl.css + */ diff --git a/core/modules/layout/css/layout.icons.css b/core/modules/layout/css/layout.icons.css new file mode 100644 index 0000000..595ac42 --- /dev/null +++ b/core/modules/layout/css/layout.icons.css @@ -0,0 +1,44 @@ +/** + * @file layout.icons.css + */ +.layout-app .icon { + border: 0; + line-height: 1; + padding-left: 2.5em; /* LTR */ + position: relative; + text-indent: -9999px; +} +.layout-app .icon-text { + text-indent: 0; +} +.layout-app .icon:before { + background-attachment: scroll; + background-color: transparent; + background-position: center center; + background-repeat: no-repeat; + content: ''; + display: block; + height: 100%; + left: 0.6667em; /* LTR */ + position: absolute; + top: 0; + width: 20px; +} +.layout-app button.icon { + background-color: transparent; + font-size: 1em; +} + +/** + * Icons. + */ +.layout-app .icon-plus:before, +.layout-app .icon-plus:active:before, +.layout-app .icon-plus.active:before { + background-image:url("../images/plus.png"); +} +.layout-app .icon-gear:before, +.layout-app .icon-gear:active:before, +.layout-app .icon-gear.active:before { + background-image:url("../images/gear_fff.png"); +} diff --git a/core/modules/layout/css/layout.theme-rtl.css b/core/modules/layout/css/layout.theme-rtl.css new file mode 100644 index 0000000..71312a9 --- /dev/null +++ b/core/modules/layout/css/layout.theme-rtl.css @@ -0,0 +1,3 @@ +/** + * @file layout.theme-rtl.css + */ diff --git a/core/modules/layout/css/layout.theme.css b/core/modules/layout/css/layout.theme.css new file mode 100644 index 0000000..1b0c267 --- /dev/null +++ b/core/modules/layout/css/layout.theme.css @@ -0,0 +1,153 @@ +/** + * @file layout.theme.css + */ +.layout-region-demonstration { + background-image: -webkit-linear-gradient(bottom, rgb(70,70,71) 40%, rgb(91,91,94) 70%, rgb(125,124,125) 88%); + background-image: linear-gradient(bottom, rgb(70,70,71) 40%, rgb(91,91,94) 70%, rgb(125,124,125) 88%); + color: white; + font-size: 0.8em; + margin: 3px; + padding: 10px; + text-transform: uppercase; +} +.layout-app { + background-color: #ffffff; + box-shadow: 0px 0px 1px 1px rgba(0,0,0,0.1); +} +.layout-app .layout-display { + background-color: #e0e0e0; + border: 1px solid #ddd; + padding: 2em; +} + +/** + * Regions. + */ +.layout-region > .lining { + background-color: #ffffff; + border-color: #cccccc; + border-style: solid; + border-width: 1px; + box-shadow: 0px 0px 1px 1px rgba(0, 0, 0, 0.1); + padding: 0.75em; +} +.layout-region header { + padding-left: 0.5em; + padding-right: 0.5em; +} +.layout-region header .label { + color: #666666; + font-size: x-small; + letter-spacing: 0.1em; + text-transform: uppercase; +} + +/** + * Blocks. + */ + .layout-app .blocks .block { + padding: 0.5em; + } +.layout-app .block > .lining { + background-image: -webkit-linear-gradient(rgb(102,149,168) 0%,rgb(92,133,150) 99%,rgb(92,133,150) 100%); + background-image: linear-gradient(rgb(102,149,168) 0%,rgb(92,133,150) 99%,rgb(92,133,150) 100%); + min-height: 4.5em; +} +z.layout-region.layout-block { + background-image:-webkit-linear-gradient(rgb(123,194,170) 0%,rgb(114,179,156) 99%,rgb(114,179,156) 100%); + background-image:linear-gradient(rgb(123,194,170) 0%,rgb(114,179,156) 99%,rgb(114,179,156) 100%); +} +z.layout-region .page-block { + background-image: -webkit-linear-gradient(rgb(137,211,124) 0%,rgb(128,198,117) 99%,rgb(128,198,117) 100%); + background-image: linear-gradient(rgb(137,211,124) 0%,rgb(128,198,117) 99%,rgb(128,198,117) 100%); +} +.layout-region .operations button { + background-image: -webkit-linear-gradient(rgb(254,254,254) 0%,rgb(225,225,225) 100%); + background-image: linear-gradient(rgb(254,254,254) 0%,rgb(225,225,225) 100%); + border: 1px solid #ddd; + padding-bottom: 0.5em; + padding-top: 0.5em; +} +.layout-region .operations button:hover { + border: 1px solid #cccccc; + box-shadow:0px 0px 1px 1px rgba(0,0,0,0.1); +} +.layout-region .add-block-press { + background-image: -webkit-linear-gradient(rgb(245,245,245) 0%,rgb(221,221,221) 100%); + background-image: linear-gradient(rgb(245,245,245) 0%,rgb(221,221,221) 100%); + border: 1px solid #c5c5c5; +} +/** + * Block operations. + */ +.layout-region .mb-block-operations, +.layout-region .lb-block-operations, +.layout-region .pb-block-operations { + background-color: transparent; + height: 100%; + left: 0; /* LTR */ + opacity:0; + position: absolute; + text-align: center; + -webkit-transition: all 0.3s; + -webkit-transition-property: all; + -webkit-transition-duration: 0.3s; + -webkit-transition-delay: initial; + top: 23px; + width: 100%; +} +.layout-region .mb-block-operations:hover, +.layout-region .lb-block-operations:hover, +.layout-region .pb-block-operations:hover { + opacity: 1; +} +.layout-region .drag, +.layout-region .gear { + background-position: center; + background-repeat: no-repeat; + display: inline-block; + height: 20px; + width: 25px; +} +.layout-region .mb-gear { + background-image:url("../images/gear_fff.png"); +} +z.layout-region .mb-drag { + background-image:url("../images/drag_fff.png"); +} +z.layout-region .pb-gear { + background-image:url("../images/gear_555.png"); +} +z.layout-region .pb-drag { + background-image:url("../images/drag_555.png"); +} +z.layout-region .lb-gear { + background-image:url("../images/gear_555.png"); +} +z.layout-region .lb-drag { + background-image:url("../images/drag_555.png"); +} + +/** + * Block information. + */ +.layout-app .block .type-indicator, +.layout-app .block .label { + color: #ffffff; + padding-bottom: 0.1667em; + padding-top: 0.1667em; +} +.layout-app .block .type-indicator { + border-bottom: 1px solid #ffffff; + border-right: 1px solid #ffffff; /* LTR */ + font-weight: bold; + max-width: 2em; + padding-left: 0.25em; + padding-right: 0.25em; +} +.layout-app .block .label { + font-weight: bold; +} +.layout-app .block .type-indicator + .label { + margin-left: 2em; /* LTR */ +} diff --git a/core/modules/layout/grunt.js b/core/modules/layout/grunt.js new file mode 100644 index 0000000..79bb9e6 --- /dev/null +++ b/core/modules/layout/grunt.js @@ -0,0 +1,66 @@ +/** + * @file + * Grunt file for the layout JS app. Allows for linting and build processes. + * + * @see http://gruntjs.com/ + */ +module.exports = function(grunt) { + // Project configuration. + grunt.initConfig({ + lint: { + all: [ + 'js/collections/*.js', + 'js/models/*.js', + 'js/views/*.js', + 'js/routers/*.js' + ] + }, + concat: { + dist: { + src: [ + 'js/models/*.js', + 'js/collections/*.js', + 'js/views/*.js', + 'js/routers/*.js', + 'js/*.js' + ], + // For simplicity now + dest: 'js/app.js' + }, + }, + watch: { + files: '', + tasks: 'default' + }, + min: { + dist: { + src: ['js/app.js'], + dest: 'js/app.min.js' + } + }, + jshint: { + options: { + curly: true, + immed: false, + undef: true, + browser: true, + laxbreak: true + }, + globals: { + jQuery: true, + Backbone: true, + Drupal: true, + drupalSettings: true, + VIE: true, + _: true + } + } + }); + + // Load local tasks; we should add local tasks later. + // grunt.loadTasks("tasks"); + + // Set default + grunt.registerTask('default', 'lint concat min'); + +}; diff --git a/core/modules/layout/images/gear_fff.png b/core/modules/layout/images/gear_fff.png new file mode 100755 index 0000000..dfd4aae --- /dev/null +++ b/core/modules/layout/images/gear_fff.png @@ -0,0 +1,238 @@ +PNG + + IHDR sBIT|d pHYs  ~tEXtCreation Time11/14/121tEXtSoftwareAdobe Fireworks CS6輲VprVWxMKQτ "h 2&bpZq,E\s74غB?@]Aĥx RP:~˜{0dsO| e +i'l%84M9=ZdYQm omMo ?h# +!^4{?8.O9OLq'U}[Vk=N9_˴kk91oI_8ۉK8U:o%/uyos%>9{G6ᐝd58f)kc[C앵؝ᑃdG.Fpҁ.V<`3}s޲,BP&=S%}ةؗDǾ^)й!h 9$G@5[Ua:*8>Ybx>$i&&xNDL2a^A6㘹 wРj#9&| +3i9102mHIGoE#hXX}@dY\}\|6 qnG&0W puK41['@VEo 0fKcӘ +ˡC|:QfDG):*V +*3橤c&{-pG+ޯIPP.Pd"џ1}b^9\.yW&;SvBy "IYKRft+`H3 'eO|^<̯cWVnA^\UwJT؛Cެַ'gC8Az^9NrADa&;'ޚIw bX AcBX)B|6K1B>Cȿޣ&{3 %`L3ӌ4S#L7imB!z;sPM`KKV9uوA5Fۣk^hzMJF*[bDܴ@M@V$ٵbgU9D&Fbպl9Ťkr\_T0RJRl]!{sYk܀`!e`~cl6]t5[wqf3:\)RF]I,mrD,Lg6 `G*'j1|Z+FcJqGe>b,@d@u8#F&eF׈D ؄Dr&A٥CV`OxN:d%i\bEV~D8ZS+x1!@"ґf(*&'O{?Tp1{'[س&/Y ѲBzppvH@Go)Wאr 3i/3iӛ S NN%8p~Fٺ0*D瞐\g&nׄj1:㘺*LHļEq*Eds"M̥x=E6f׍}Zxjo-VaA~O N ܥ0fĔlQD`aEXvXV˰9泀yf.Bj謃isЮ ˋ'/3̔t&|&X+/h&dA51U#8 +^WyV0;lyN_f2kϨVx2XYI*0ɮT5DmU\)xSc3q>sA;i}g(y -2{u t"uN:eʹ|\BTyFRafGOJ=?"D*":}Ȧ_bCi}n]_H6{1}n32bXU钝]_k/uy:$K[ {YMoH@(7 V%|e=}nǾ5o%0h"1q*='cc%N`SNvAvz LƈgIK\8gkFKl)ΟԜ_k>1LS8:HvzL Ži`ڈYd)ΟԜ_icZ1m =L^9~y٤D{z,K㎥k.gb3iB\+aJ:v{yRöY|qE+nXXw -\V<^qzL[2-;]"WsgyRp?#N10:0eoG^^6I|Ds}Nڼzǡ]v4to3RQV#i] ' mإUmx-rUd(wEWy]0*Uvѭز採ѹaqOd +|Ub7bDU?Kܟs[dO."ʖ/7, mvaZLksLƄ-+o50 <qe aZ'xw VwR$lƟgAj Taw>My@)_HnogV+G%⣬;jw[~òTҹ~ї94SE +ŋd]qCЊtZF˫E&YΥΞǡ=s!KZ2p!U¶##Uݿ%y^s9TC/%ps/D+ʯA2]̓E7ӅW椯-*uTe? +q:q@#h=GÆGmGFzKO-;*E_!qbku1Ҡ^֕wFO;t{Nkѧ O̮-y[`3~n??= +5ʒs?oz{mFe|G8ù^J1=^pźE*DrQZ'Q ³gl{C]BlzNb΅pŢ? _෕b$gճ;6r!?aVs<&3;@޲@Hj^ q9ɹ Wx!=Gr k-]W3Z$g↌UT4{Л7䩗.ؒwКgb{*nK8"?xEupQyLI>l+/Vb'OzMwnas 3i/=ѝmFA6.ZKg;[gΝusV@ߑ(,kBw YQ"aj;ruCȼ~P;Ǣs=Rc% Hmط:cG7to^mD]" k 'ՖE"IؿQ~;펏?]o/fdUq}v?*4q*i>,}Y+z;{K{r$ֻo{lw \e6jت˨ݩUUfB_~U\sJ.ʯ QIx_p+.z?+~f[w0F[Ҝ8 +{>^ol}ۇsM?X>\?/R4WT8 X]CK޿>[J)8 +~@G=b@S +b3EW Xc)IhK_so틒 W\9} Ya CJ/fj1"IYi$5?%YXP\䑕H*$ZIx{!)o%;ݖOO^c >+y6'V"pNRPF|9~_8q\m6GM^Vę◳5l&~Y f[ '`kYT?LvYZpkț{q\,^уWT }O"kld5}5]ƈjؾCqx=Bƪ_]QPyFЖ+jvyY_H>]j+,(F,UX;WwVv_-6&FrhMk|nSmކ $YNה^K+}~i ,r~i$e5C2wWz=rрOIIsQMzrdaAߐ|qLP?daN@ ]޸,|ҟ{kpwh8HΆWW[2Qd$fшfрV4 x\9"ц'8 gj$oz5(tm=֏ Yfkͽxdʼɝ<=0x̭oO/#!!߀o**iק2x&)r.mVU tߡ]{5g)6M߶t%9vqvqʾ=ߧx ;1t@Ss}=)};>@"{LՆd$P#-"^x8n)OEC6;X#Fpj1ZS~tsoSw?b't=آ-6XmīChƻ{32D>? d\0+hF44فY8l՘"&#Pe +MNOǫ/[/A~'}q]Qq't6>'GA?}lkzݴZUڳx{wvr[knnk|`2&cpL;::{',8:M/~pBʣwx{pɆqN|_wkNq/iߏ"7įy<|Jd0Ǎ1(6ߣ잎CԷ /u u7az(x5ZLz5Df`ky#Q^ؖڞβTPM64|hB3I HL#GQ1kԪoU ҷ?ݰkEF8q3|녁LQj Ќہ9N jۏ-'=BZ:ܾe{5}/S"ߵ\>(\~خ%zVlPEKcvѳm7 e. 6?b7AiPl>{{~+*uMAOVWE۱9|TRSiXV B 1eXS oӷhŮc؅nӘ~;jݘ]6 ̆ -9aؙb>8 X\ *8t(vř㌦ą`k ^? ܜGn̨zUOq(02ľNr*7 C~Adg$A,UD0U18 HCVnTJlc< +++Pj9V^V8H[.F<Ê,!h(g$m,5y0YŠKUhyA=>- VγqQ,V[.mTjN,\X%XzmʨFg2?LZ)7647V몈q9 E'c4NmQ!:h 8sM@+}U/#TZNaD gQrubqyS~8ogD;Ige*d[Z~I;,TW +ÖFj{;̩Mypp΁:8N SEzgQ68LBя5p޳! JPiv@eO>K,^Bk(,1vz'hc(9we}HeޕT"\>g>}!+;q^3õJbbXCIL!iLKy&X W+GH ?&qj@\Q!KTw95NIBnlIB!I$a4Dpe^b +EW1Q\RUES%,*Z5RB8$ *D"*yObrٍՓ !-ѕ ㈃Ɇ0j"zh (Z hHbݜa )i R/H1YÂQrfy  r&b*5L +DefIky8 .Jq5LAzf29B-Riԥ W"szyϭy-7kź |C/וٵXp/N!#TD$$Ļ{ wY5wGG9[mdb[]p fp|ɎَjCN!X;÷ _Z)>Үx NgՋ)]:Lq)g]?!8k8A7 v{5]>9;zIysz>elћ[ew_zppaHiJ/ރ>;HCNLh`D F`Cvr:{Հ/3SϷ+#3szO &t%xC׵@jRq%ua9 !!1t3t +98bØND p Ky;X +Z3v̡tI (AI L'61sb7+g=?/^,]#6Z566$JַdtfbBLX5v +.t r+ +(&wi [|=d+Y IQQ,EZSĉV*ن +Qjj7#|%:$bR>΍3*FAݤFm=v%b" ]xƾs}`oƀDW1e|Gm+$Ssxܶq )&ziqcaIeqIabr^tĹ5Kp\5Y.fhlYmqiyx@fKcӘ +ˡ;w]n&f"Y0Qdbh +ZC+` ~*n̰ۈbPP.Pd"џ1}b^9\.yW&;SvB?iIYKRft+`H3 'eO|^<̯cWVnA^\UwJTcqr61/'q~hs&;'ޚIw bX AcBX)B|6K1B>Cȿo^o G <63|ӌ4c0͔~0CͯroPM`KKV9uوA5F+?'cQ#-1"nZB J TFEӊZ3*7 +M%*Īu*r5I&g帾$aJ$\pe]!{sYk܀`!e`~$EWu'`6ɕR,Jm$Е6(gY8XNrt(a# xxrç% OjM"cH055qڂ׈d ֈ1`p`H$ (8 +L IXsZ-שX60_*7λJ<y {{:t$Jɉ#$&e?~ &liIKChoPkn5[5dk_#7K{LZ#T7S>|Q\tPtM3@۩$gΘhU\p$q|sS"5ZpPE1ܢ?DzC^?q]mten>i;U -!}aEib"=EW-y);A|,NVͤ蓛rƀ*:=˫@>~gOM '4̪7Һ<6+cKic +.5-D23/&ڨV}ܶż$M9JNY +l7XS9#:ocN3g//z40SFәTbeO,T*x鶟^X X4R8}E ?Z`e%8'RPJ | TŃrQL-Q9T5ϷH4-kӑrAJ;mf`ꤖ7jCq RU*ZcH]s=}*NIXN|D!~`&dKѥY;cu]s  pNZ<Ȯ4\[c1b 6I3s)kgl~hM1qg\;]ic +7R^ΟԜ_i!cZ1L091-bL:Gi;]4+/8TwhX1ciܱt ,\|6_q|v=v%]IǮ5u/O +Qs6k9/|qe=-.~şˊ+ΟԜ_iKƴeǴKb,O +qdIõ2BҴ{Ck3鑯hIW8Ky·cƀ]*wj$k4 в2VZ}"* Fr[e.[1C3:7=Д^OOTV,eW=ī^.5smj] ?b?X¿*[߰4d;مk 3͝3:Į^ 4z$NPq/dTJiెka3ԛ[JQxo^\9mp$_Rjq~{\*7U|"EYVC @m SIJڦE_ҬN%)\/򓍚w +C+i-γ:Td9:{΅K/ojifĒ…T rTVwxwwC}ĉJz :WXW ?]JөKB9F6? pyehOߢ QdI9ϝ7=&*#N/ÞlE=+KO!e+;Y³>!.z!] z6Y=|N ~s z)NBbџOw WJh^1@~C+'柰GݏJwg+Yf?| +NoX=p 5}zD[z}+<^K~╖C-Yܐ*{XߕF~zRÅ';hf37} T ޿)?x5oQy#YIβ-"K=խ=\F+n__zT|5ŧ O G[_m8em&4)׷T7Vї鋣f?T]OH@@ڎ=/{'XP|NտG +4[{B} {^Boѓ{p=ʆM }*U[E +,ƭ`ycF}z>9W<3%unJV\:r{N c[:E^e%*΂Z əm~AGuǽi{{Z*U%DWU 'JJ=ggv~t_:|a_| E{cde{w!vo5]oH͉>d,G#];hc䯀JD_p+.zy+r1n}~^p2!gY*6(ie>;fj* -X)IhK_s틒 \9} Y0'6=K'pň$|daAGV"%h%a톤뗀v[?U>yU6 |:n9GISrVy|u9~}nr'Y1,`;YEhը?nC(~9 !UByF"$#%x:[4S~;~8tFME%T/$D!fKsR*Œx85$9Ǹ1\^JN"_zHb滩u~X*7y-={ ̯֙}B'BygoܣqdgnpdlM0CtOO0d)w{zlOq0/mJ1K8d6QAGZ:ѽ 'p:"S [x.`ElvGx?0_b6?:qp;ڧ)^mN1p{Eg[mxbt^X}u|4f&>%Wн!\w7#7 b>;i LiY|\n 4ӈ/@)!@mxX4]ld¸}{pvp9`qMr9|N\G-Ar?}h`|u0V!m'4Nq>No.0)\;~W)nQ51`OI}<$zp9 l>D}+YNݏ]ׁPwY0~ߋȉW$2yGDzY4b[j{;ښSC6X` $Sj&"M3vqD^NZAS:V0H +wG6u~ 3F@3bo:냨n?8 +t# + ~h9hZr^֔8LiPN?|r~ƶ/sa~YqA-E϶0 +,G۔w؊=TЏ#Ay߲B}e-45=Z_ijLmQIM_bUZ5A?0r8_lccMGN"bMc +xucv|L632׶A`g +C׋ ~`q%r <ӡo۱dg3nB5x(B7,rs +1V=Cuø +:gvu, AI TvTTLt26 M[R)9U(XRrn@ZyZ me+vH|\V+,UXָba;?d!+.-7W,$tZ9BeG]Xϲ^nYP";9Hs=c\K`)Zqfl"p3aja, ܌[aЮ"dC_NUl;l$$~w@";)F@DmJRrSN&76fAT+~ @}%zv==V=[W3XNg)H +hѧljUm-z?A/n5[mmVJ{p_(0|wMiє6Я?C%`.QAmkBSx]N0QʫajohlU&fnÃsfd,>Op3]E1T яjV2ڃ-p uyGpodl`d/e>v +9]Aݩ6ٞGÃT{1H QlH':iуŏpa'D:__CEymkBT8x횉m0]HI!)$FR?6c>>~sm+vuՑνYu8uN?WP>1JsWiV_uKEϸ/rˆ_gKW]ױEYcl,[TYHT}xL#}A GV7^}>iҞ-i;}LJX&TP3T#ߨgJl e'=?͘ona|7>?ǐU%;/mN/IfQփz{G}?v✽3X~j{zTAO^ʰ>?sy|G)P׻gI@XjjgiЃ`0 `0 ?ϟ|:seQ3|ӧO|:2|.};7eGFO6_Qv]T]^ˮg{>pjzkuo{yye?{-x/ D:3D&򈼹e^Hyi#/OGzϪ߯_~ +:sMe#M3Y#=2 QЙ[\s=E8}E>GȩT ڲTg-}VfoSVwzV}./>~!?U1<#}=F[ ~QڋBN..+푹^edLo+[\-k dW(}6q$#?z6Bөi?L7!3O_Q}Пuo[=tkȋM!'}/Ƈdr2_Cﲨ: `0 :8o=+8-4}۞cĥXdq{bUq©ήm!ƶg*ΪU\z[GA=^+ru{LV U?)V>ғ)x|Yҁgi\yi^cUo*= !TY?rfgWsʽVn*VX#=Fϫ+[F~yH\L~[O҇h5ݵTow|Sfӟ+);F;:x )/OS yUo2e)Ve3'wgGg=J^`0  +ľu kU,Ksؑ5nY,bXw{ w&3QהNQev ]ƷgcH˞i{A3I8hwduwUIWq8I>+@pQşGcZ\ƪUߝ]/:3d;ɫ:gB9R|GW~w2;fzt|+i5nΟgZY|<1NyŬ|E7k?z/k><=Α}N΅>uWydʬdz `0 *\?W8GY:Dgcg< 2+'W6qn؟{ru"wU쏘~c#T?+y{Q,,^qF/Xv8.֩g3}ȸOP ~n%hUG4(_sn|W}Tg&x^c,Fѭ+ <#+}/Uw8BRh_|33!mr\7U9m({ѝpvew[xG]߱?g;,nҽow8]וb?OV=Z_#ve?vN_WrYLo;1g9pV^G~>[_vNOS3 `0Q[ veO\k^8֔v<Zbz\Opbn$~}oz3ј mK vU]^iNWA#x딫jt q :E= z%օq)CcYEqyRG-+u (K\hP'*^ء^q=m=y|Kvūe\rȊ4={W1;=ݷxp;o@>ȘT\Ԏ+C=*ɫ|GJOCW]x1.ﵠ9_Eб Vq)v(ʑ}[GwǺ{-oSdו_˞׃2;iT&w*w:g׭SOsj%Z[~_˯d֮+w]7 `0]kIu+eL]ւoA^;=GR?v쯱;<y o$N1紈=:ߥPVu< <&3KyC/4r)i=*/|Ύ^]QNН1qGw>ù{ ?Kv:A}E:_n+{u=rq͓̳]>>d}+|L01`0 leg:׺񶊝`W,3O?]\9P~[kOWiGc~)-<w.3q}'vuw$Vnv(r52S;Wk_Kϔ8B/hEՠ'9w?K;x:x<|@cϽVyc@ۖSw8Bq]=2lBe6V}eR( VeZT4ade2ޒ+nYBTqSߔ<[&=f[|szP)G}{Zׅ3n7jpWwftEw[ǽ;`l? `0 `0 `{~i`oLy>uoi\qK|}7Svu9G쯿c¾#>,jow{ՆݲL=mW2u_8دjo?kD߱mw>#}E:OۡO;y`$j?tU_mkBTsxځ @FhGpGpFpFqH=;OxkvmX*}\_q\>߇ǹ+!f4cӶ>saTڧ?Oլ}MΏ&w|-%_Xm9-׽^i٪nXg?WO@p~_<)el-Лz2IY +mkBT~x흍8 FSHI!)$FRHnw HYx3ꇤsaaaaxIǏ'U{o_ھgW9 o'GW {>~Jlo߾)*/N\ϱov[iZ_ձaJΝ/:6O- 92b?Tlk%?_21B sY5>:>c=1Ow y^- ڶ,XzusM#גU]>H_yYv!ۉ_mi Rus]Xm_g)YY)m]y,m z1aaaxEߓGקo/Y\k6xjgH|yu.\aæM&wk#ϐ$?]Mo\Ⱦ,/ڥQ@~6s?)}, l gX #vQg Bٙ^uのuhm?}{].~}v_J;xogJY]޳@.)oqC?}>@Xߘ'-(W? źvƔOʙRv[K?[A}?-wmՑ}g\=c}M ggg DŽ-B^k_g?F? v0||؎=ǧHPgs/hؑI t~{n^}ZyD5XWvO)"c0vY Z|~_%/,p\ɹyΰZ/;/xs_9?Pܯ5ݻ\[y|č8gʱL{? 0 0 _k3>z_\S |<)b|7aaaxn.ta?l^Cvkؽ#~e)3<3^kdlc&jK+o"e<.ʞ`^(3zu +l+6v<ï k7]/lc[`On}򚄫 G뎱zt^v2)?;Wmr5ocIz?Ozx{&!ez."ѯ 1Gg{+ҏlw<=}GݽFƨ^)zIpG K֜{{e G12ۭqiumf>.}~a? 0 0 [u+7Svq֭y΅ +?ނ}XwŶv?ߩDZۓ-q/?߳=<~#>Fk"qzrQo 9r,nY[;o:)@-`ק-7({߯S@µK9֠ɸ>:n3 +_[_*mtcmC>qSL=<6;ǫsaaa{xˌ\ފpx?0׋#5zяc]x^l򼠕(f:~٣^lin59W~\;?vn6erUbS~v^U O7O(|;+SG4|?f*?rW~2oNٟS9~daևmH6mX[J~s.ym4ٶO|Bd/b5ɿyU? 0 0 0 0 0 0 0.P~*1@G\⟿KrKXs2(ߥ纎J8'>X@▼QQbqwx b)_K|v 1M6kee-2Ǜ59?K^E~9ϱQﱮYF8N?~;:=J<-tĒyNAgC \NXKs)'^Kg\~2}6}Գ)n]Or^j~"{p29w6/.z-v:+M{WJYZ굢`% Ҥl9ힶկ#OUz+U?;sd~vND7*.Y+v:ye;8}~|+ÑޅN9}{Bƞ#txխsXɿkSV/uJ=o G<ջL'L:D]6jfgLz/+ؽ[{rCMYq~[{yy czA;w9zszWHVax3 ף2iTXtXML:com.adobe.xmp + + + + Adobe Fireworks CS6 (Macintosh) + 2012-11-14T20:41:31Z + 2012-11-14T20:43:14Z + + + image/png + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +̷qOIDAT8 0 /Ul@FLp}q I s8T{@bH2l-(K2|R VΉ@]ٷ + x3 +`f]jBv!9|oKg|f?t_8[vh=I5FcZuljC>XZmw:sLaeN_+xg׬Nm54>dhIuۨ8{qff9V-8+˞?_(㹴ާ;Dg^L]4+<#yDZ^;wR+eҚk>0'?#8G÷;)7ֳͩ%sOFw$wO&!-cG=^K}GqClx6' 9r=`:ZX"nT{ c>kr--".O>㦞:ӵJz9RYltu=so9ֹ~<ԋ.NH*(8&'ǯD%XiGxfPdUu.ɴs͟i=cǟ=gwĿ_?!/;~=7W|W|W|W|ŗ',~\ůq___o$G |oH?~&]/aψ>C3 ~C5 7Nj}&o:_d <?]u[?h5])rLicJ>%(CH|:ԯ=Դ8lHmkBF)3Q^zC:NKCv?v8ZayXtuMؠC.pü6 G6Ufi?B8\D{4Ϭ? і?!:72fqvs@ ?Z9VsB zGi J6c1| =}Lg !!d2̹3x8a2 ǬsL`xc8 #陡-HšǓPNl%]_z8P7{\Z݊([^y {f}M5V趖ʐ$>@$=pXΰ4ii;tsʠ' ق +5k+]1ȪOg+i1[HeM:*En!FpB6EHTRZ!uL]ibwg0\Ji +*&Yvᣯgf75@NЦH +=K'RoNʫ`3cj;wJxn Hj#9 4ߔB3 ?r=QȀ8d,G$7fYf!w.KcF;A\L0j,!f 3qƬ,'E4'z>ǖΠcN@t/V h,]3r*>tdV +9%2tZ V`] ~{BW1f_ہ*HlbH&ZE( EbR-gh< qG+^k^L] Fg9AOT(Ӌ ie1Yԙq/h>94lW.upU$pF6 \U WHR{c0⤯3pO>W,x51+:i|HP4*FeШ  4*gcʫOϾdB֍({׫uSg8J0bLb0 &Jm42B(9_!4S΋beL͚lPN~.'79k3lfH6v`zMءHaKHj̐e~SvFŢqwtEoYĨu*m>z`,jׇ$T)dJy;f[{,ah\A7 B`}:)ѝ=G +my֕[uqz3y0kB~:Xjx,,n3߇<rH xpb.#b{8d*#\iʘ T:хڔLja2Xm̀ vb:~b%`}q:pL~n%B !yהX>aaOG4CQ4 0))~Qx^RL\Im +[ƈ3+C mӼ FBL<̗cfE-4LО +k88F~}8EgqGE, +5d:CN<)%|)?C@;'}JqE Pz ?'43̝X,f?w`)sgmI51f!POc Zc1Hz/XxprOV '?2 턡MjQsE3S„F荰n/pp66L7"̙m? #öFAIc\lHez@ D؟32y_8rҐQN`,{z֣y\Eu+zAdB!+o!,ej0l1tK@ؐԨaSU_ ㄩs5AG{W86IN+,.jq6̟4|a b acH(.. ѱs}+]gׄl12L'aiZ/h5X)O10dPfU)10/{+ + Czc(in4ԇt:s3{tЅ %f'-]i+d+0*ǜzGG2}P5)q' $Q,lےX>t' =2sFL:zCD:h3j+^`Ev%ދ59hg_h_y^߾o:'M<cQvA>CxsգyWjMW$+R-a0Y4`vc~IVa+ +􂺯6bKJJC-;>IG w"@=(qi.nDL]M ˍrz?ߓ+[$uvq i[E 230iZ'! _٦HEi )0XbkJW@_ DAIS ?2$6MFKq0O +?q)< ~d`oSNV-eb ڒ\R5xu8pDǷN,IҊL2~}ӀGbq( wYD8hN- $51n1zŽa ,mN0E8V!؄*ESNRSUIgU^PPI01xx|7Qg/z{O|Xz+*#I߿i1M}DwCo@~#ߐԨhJ5L|F- +{7&h ,I<2`+N/q E&j4Z7Ld =S@QՒ riMդf1Yn <񯟤\&&Z #`,p')׫ fBkU}.].DzQ>,Inj"m1jVm.6^3^BWk5UA\[S=m0EÔj2Mk`{F?g/~rfLhZh7Y hIFZ5zL^bm)cKyMS{=bINzJj+oBZNaxvGQ(lzi26_~_>{_6뒦MLpEXoMѩ>H`}CXnVZ5Z^x/yZ$l6n5qL6DpIXkTH16G`⊨a*j MG#ԞxAjc4Q04)TaRyʘ%EE)ꮪ-Yu-`h420YQTkTnt7$kޢK嘿U~DoJɥz,0^]"ө3B`ֆWfy'ngNh*3G+{HoF;P?kI E_Uѕ'6p )*Z 5Hy[a; ?5N$3N`bGrkΙi*Pf +ZG:ݽp5)uO/yEz;*w0b% "/?RO_.oriH"Va-/g )h#9i%i؎^lKV[^'KcO&oAy5k(>ŒE~;ܥ;)Y#R+d%#5GZ(|F8$^ނ|`6ICw?jyTKn%~-o@091< .{[[ZN ʟr7º%.w8CJk9zm]st%G ]3ڨf蚡 0m=↓ٹf祳5;\s;i\m;|vjvٹfэ!>W3tfF5C ]P8x~9es^sv5g-vV30tf/C C+jW]st+YyW3&0tf蚡0׵fM`z]5;ac]Λʻkv.zV ]f" ]9{38^yWsv!gw!Dg2kl~p?{!lŢw܉[g޼)pC4o53ZHV{>mt6>|u|]Z?cm{-:\"q㋟3IEEDvg>GKTvmO#h߀3Bfh֬f蚡Xb~ȧ4ƤMb&3pH:g-ϻcTHmΈwM+>¯\{ۆsn ++2 ^³Ή!Oˠ~8_[m-h{W[*hɽŔ,>R{4FHބ_e1i4`GŔ, W1)Ӗ*w'v7?mzN٠kHǐ]!+"wXe匔 +P7q d +Gjv{Eg<+S Z"yk +r};5d)G0%| zx\ϕ_ľRN1eDwL= -^iL]TeT.GE2[l!cIɗtkD{Rҕ-!}v&{̺AK[VY{I_+e*x\oF&JSҬ˂O_)[FGZ*'>)bQJ_)}i%e{ A!Hy9"c}v"a d/A#iX?N Fce!1*X#bi0~7~i\){mEJ>GyxCyh'lsEm\m36Ndk{lO0Ůy\=Yrw]v߅ގڄ7~57MksmpmYw+͊cSBLPO(rZFƍeZY9[ ъ>V|,}梱f]JE؊̷ClRRhMEkpU+Z6pWde|Iygo#ϏOr UKZ6}mh%5aa mH{ SNLbzdEkc FcjUļogs4B=o- ĔuqۘAܘ#N'mUhAz {vY?,*|9)?vFvFb=sC-҇fWHo*e$ī]6c7!k_lP{GOXvJ* ƽs/ $ k"uek SFˬKBpVgÔ6#Qil)F E;տ8hFl5R?W1i(-뿭}.}Ӌ+Q#Q&&iUٕRא͚{ }Amʮ 9 </^>'\\swbOe(!~ +=_ombw;'-BilrV6 v&Į}׏#DK]LE// O7!wcSF)l w7Gs&5NxYv/h'l'lrq5.{Ծe`?b IFw}vp;;: g  yvma0{<`tiS3Vn㵾s.;jo9bsait?:ěXGRgEw~sŃ.d:{B +[.Н~M &ENҚq}%x/?YT6zJ_nz OIFC 8z +GҦ#ΠW1;=]Q +5~ѠQvT3;"'j0"@; Y`6T)* MPM0k\BaD H#ϺsG@Rᅓ!>AĈ.Yr~ :t(an=pG:[v~}||eSn! Rٳg FtmO}8PO<5of|?댟1ԏE3ឝ%}hgz{OQy5>w~$Ogi1M}[pn'9A?x ã.柵)tSgsdjCU8d9>vL4Ԗf@\Սm|mBNH07mT>rxn~AyA%QSX95^Μ꬜YWԚ*lLr6VjPҞ6=R[ٚ6 N킵$9U/8 +UNYUTfD̤S8$x0t?>Emּ{B#YW$ 7CzcH~nqs+$Uvtc_ve W{82sLCFF7`auA=GCDZCF|cиCfvt"?,j5<&g]>ڴȶDSy TeS;&MS.ұsZVM(`jށ*3{!qO>]䐍fmg97`W{HAG{$ aLmfG#=ݏvGX+~5NXt :4l= < `0Paqi?B %n=gVh՟ꏈQ Z3F8F;sZT:dc;OdPz.>Gcq7c:CHg !!dΝ &Uǁ1\8fpcPûC W陡-HšǓPNl%]_z87? +OJVSV3 +Ӿnft[KeH [va, [gXൗ.C7 :{-ȠPK"*@~&H qT$Zb'dSĉj @%u+U;&̰T3X=[젺=cK):MA#.â1| ڌvxX2}#hS'T9^>"&~sR^SYSue@PS[ЍJ!FN9ў(dmHI#3K,Kߎ;e1#ם u.[&Xn5 pg38hdcV@ne|cKg1 a +Scxn+O$]3r*>tdV +9%2tZ V`] ~{BW1f_ہ*HlbH&ZE( EbR-gh< qG+^k^L] Fg9ьDL/.ŬgZRgƁzp0:Ұ'_=ԙ_U^E؀pV1\#)J] +5{#NpJ-Nq +pzOVĬ!AeШ A2hԂ<)>qWW?EFCbY7}v2{`L.e0RۿG{_.i>pDp6ߋ+`14kVdA:p͠#ءHaKHj̐e~SvFŢqwtEoYĨu*m>z`,jׇ$T)dJy;f[{,ah\A7 B`}:){~wrYWn d +6RcgU8X|tva#k4c‰>`VFw\%y.щ]RdeLL\AB|mcDl0Au6fNl;E? Β~i0>).f!N'ҭ~cZ~!D9+1g2@"hБf(JA&E2/ +^w[Oj-4wV"%0FYoZ(n,1Pg1d6+oa0ڞq? | ɂ0<$͘sk_:s& eԐa:KGZ|QG"EsM̅x:7;AIVUۧĈ|H*,{z]Ѻ&PRR} %>BZ7XƖ9N +ttI LHs^w4P$7oq [ay[>".7qHBf-1 NwB_1t)ȇL׹tf22V +ٽyDoދ59hg_h_y^߾o:'M<cQvA>CxsգyWjMW$+R-a0Y4`v`7.[A_* 꾾Նhu:XIiHe'D%sR O5Ln- +K @'x?$#M&@ zVv =S@QՒ riMդf1Yn <񯟤\&&Z #`os^?I^Mh6] sw| ҋ/d9NrsTiUHosi."B}PZM\&I- + +B9lχ)?TݔnZ#3}8ORWڌ mV  ; ;M;H&@ ЋU^M!]k}OrҫURcbmؗ1WѫR]fK Y`\y׽ESg1 8<,N*݁su ]ţ=h7k7w;O/XCz+ځe\+MJm(E ;OxmJZRT(gjgS6*#w"9V*gQ,Hn=92M]= N;Hg/}{G.F=z9Gj~ %mS.-SqIC*,0ZyCc,G +#9i%~)M(vkt:d^򟀿5'*?yL{4Q~ ʫ_C,)}/+. F)-thZ!+..2.'5!]$1Vr^G#IjҼTxo9S5V,?=Œb\7MmoA'S?kQf;Gl+nG8(;oqv~A};[5;\sPafe3tf蚡k.?pv;X~ZOo9es^sv5g#]3tKghkkkfh&+ﴚ7w5;jM[yW3&0tf蚡0׸Mz]5Gq|+jvvW\s׳fM`z]5Caz]ٛʻk.cU=+jW ]3t]ȅgv@wxZ-[o-ń)^[gk`݉[g޼)pC4o53ZHV{>mt3"|Tt!Dkvi]CkD׶#Zs}Ø_:cCИt[Ti?_KTlǺ-zhjB߮-t~)0t0pF~ -ښ5C ]3t+X,CTfrØ][_D;]~jZ︰o=FVW al36qG>46f;qlQH衹=M U<Jh¯ kax%:^PeREROC=(v7 &40_J $9h=4RSC1;FCݛB +k{mm3<csb=ˠ~'/x-j6 +z =E+ӂ1[m!ƴh,HEo=&{qh.Lx#6 Xg +BlD/g H!%^+ej˼VߠVb\8|YI#?e WVT+aORZ볟">zx`͝XRVPDݍ|1+q k/'} O{ "IwbHЖ5JbA /_yL7}d1{Xi1c}E;4pw9$s,)Z.𛒽l"ܽHuʝȷms߬ӘNdoMu)< 17(qVOrVWZdIކ|S;ɨV$43[)o+Ű,`cX@8uCrF}b7@~9;kea'Fy{ȻDZu]X٧>[| &!Ŀ +-V!dkW!V(eyw S$OHBg۱%bwccF%|ٺd6|E`q5Ϸd M"L2R[ao'_zDDtWFſT?_$P"$-K?3~/ڪ~/^U6r.Oy<,fᚅ< *c`gtuϮ=Oyϼ7X{k{ȕĕax| ۣsxvZ]2}v8:}2S N5HҠ?jA|Nvtp63{}zo][xt1; ^9Dž.;~sԌsx}qn=wX&#nvQґOO{xp؅L˝)*о < w~NҚq%x.¿㞃.BO㽤svT3nnz OIFC 8z< nצ~ΚW1;=]#G{*pQGG7SEpj;iSwjwκWȻVX}+ca9%At`']K6 v}v4P")uxᢕ;pK Ss@NcN/qObex>raz}Cɻ}x`\H'&Dip`T2`\]%('9Έ{A C9OϨ/O<-68| 0.#IdӞczhYOs#G]?kiS#φç w4p Ɇs|`™f=i-̀<=) 084>]@E'o;ʩJ-} +tzܢxEmּ{B#YW$ 7CzcH~nqs+$Uvtc_v=8 l)%;_:lpN|G΋k5Gך5)̻4--f``oVҘf[Y^wS܃&W׈+ +*F\_W ~t_.=ruie3s̛mkBSx]N0QʫajohlU&fnÃsfd,>Op3]E1T яjV2ڃ-p uyGpodl`d/e>v +9]Aݩ6ٞGÃT{1H QlH':iуŏpa'D:__CEymkBT8x횉m0]HI!)$FR?6c>>~sm+vuՑνYu8uN?WP>1JsWiV_uKEϸ/rˆ_gKW]ױEYcl,[TYHT}xL#}A GV7^}>iҞ-i;}LJX&TP3T#ߨgJl e'=?͘ona|7>?ǐU%;/mN/IfQփz{G}?v✽3X~j{zTAO^ʰ>?sy|G)P׻gI@XjjgiЃ`0 `0 ?ϟ|:seQ3|ӧO|:2|.};7eGFO6_Qv]T]^ˮg{>pjzkuo{yye?{-x/ D:3D&򈼹e^Hyi#/OGzϪ߯_~ +:sMe#M3Y#=2 QЙ[\s=E8}E>GȩT ڲTg-}VfoSVwzV}./>~!?U1<#}=F[ ~QڋBN..+푹^edLo+[\-k dW(}6q$#?z6Bөi?L7!3O_Q}Пuo[=tkȋM!'}/Ƈdr2_Cﲨ: `0 :8o=+8-4}۞cĥXdq{bUq©ήm!ƶg*ΪU\z[GA=^+ru{LV U?)V>ғ)x|Yҁgi\yi^cUo*= !TY?rfgWsʽVn*VX#=Fϫ+[F~yH\L~[O҇h5ݵTow|Sfӟ+);F;:x )/OS yUo2e)Ve3'wgGg=J^`0  +ľu kU,Ksؑ5nY,bXw{ w&3QהNQev ]ƷgcH˞i{A3I8hwduwUIWq8I>+@pQşGcZ\ƪUߝ]/:3d;ɫ:gB9R|GW~w2;fzt|+i5nΟgZY|<1NyŬ|E7k?z/k><=Α}N΅>uWydʬdz `0 *\?W8GY:Dgcg< 2+'W6qn؟{ru"wU쏘~c#T?+y{Q,,^qF/Xv8.֩g3}ȸOP ~n%hUG4(_sn|W}Tg&x^c,Fѭ+ <#+}/Uw8BRh_|33!mr\7U9m({ѝpvew[xG]߱?g;,nҽow8]וb?OV=Z_#ve?vN_WrYLo;1g9pV^G~>[_vNOS3 `0Q[ veO\k^8֔v<Zbz\Opbn$~}oz3ј mK vU]^iNWA#x딫jt q :E= z%օq)CcYEqyRG-+u (K\hP'*^ء^q=m=y|Kvūe\rȊ4={W1;=ݷxp;o@>ȘT\Ԏ+C=*ɫ|GJOCW]x1.ﵠ9_Eб Vq)v(ʑ}[GwǺ{-oSdו_˞׃2;iT&w*w:g׭SOsj%Z[~_˯d֮+w]7 `0]kIu+eL]ւoA^;=GR?v쯱;<y o$N1紈=:ߥPVu< <&3KyC/4r)i=*/|Ύ^]QNН1qGw>ù{ ?Kv:A}E:_n+{u=rq͓̳]>>d}+|L01`0 leg:׺񶊝`W,3O?]\9P~[kOWiGc~)-<w.3q}'vuw$Vnv(r52S;Wk_Kϔ8B/hEՠ'9w?K;x:x<|@cϽVyc@ۖSw8Bq]=2lBe6V}eR( VeZT4ade2ޒ+nYBTqSߔ<[&=f[|szP)G}{Zׅ3n7jpWwftEw[ǽ;`l? `0 `0 `{~i`oLy>uoi\qK|}7Svu9G쯿c¾#>,jow{ՆݲL=mW2u_8دjo?kD߱mw>#}E:OۡO;y`$j?tU +mkBT~x흍8 FSHI!)$FRHnw HYx3ꇤsaaaaxIǏ'U{o_ھgW9 o'GW {>~Jlo߾)*/N\ϱov[iZ_ձaJΝ/:6O- 92b?Tlk%?_21B sY5>:>c=1Ow y^- ڶ,XzusM#גU]>H_yYv!ۉ_mi Rus]Xm_g)YY)m]y,m z1aaaxEߓGקo/Y\k6xjgH|yu.\aæM&wk#ϐ$?]Mo\Ⱦ,/ڥQ@~6s?)}, l gX #vQg Bٙ^uのuhm?}{].~}v_J;xogJY]޳@.)oqC?}>@Xߘ'-(W? źvƔOʙRv[K?[A}?-wmՑ}g\=c}M ggg DŽ-B^k_g?F? v0||؎=ǧHPgs/hؑI t~{n^}ZyD5XWvO)"c0vY Z|~_%/,p\ɹyΰZ/;/xs_9?Pܯ5ݻ\[y|č8gʱL{? 0 0 _k3>z_\S |<)b|7aaaxn.ta?l^Cvkؽ#~e)3<3^kdlc&jK+o"e<.ʞ`^(3zu +l+6v<ï k7]/lc[`On}򚄫 G뎱zt^v2)?;Wmr5ocIz?Ozx{&!ez."ѯ 1Gg{+ҏlw<=}GݽFƨ^)zIpG K֜{{e G12ۭqiumf>.}~a? 0 0 [u+7Svq֭y΅ +?ނ}XwŶv?ߩDZۓ-q/?߳=<~#>Fk"qzrQo 9r,nY[;o:)@-`ק-7({߯S@µK9֠ɸ>:n3 +_[_*mtcmC>qSL=<6;ǫsaaa{xˌ\ފpx?0׋#5zяc]x^l򼠕(f:~٣^lin59W~\;?vn6erUbS~v^U O7O(|;+SG4|?f*?rW~2oNٟS9~daևmH6mX[J~s.ym4ٶO|Bd/b5ɿyU? 0 0 0 0 0 0 0.P~*1@G\⟿KrKXs2(ߥ纎J8'>X@▼QQbqwx b)_K|v 1M6kee-2Ǜ59?K^E~9ϱQﱮYF8N?~;:=J<-tĒyNAgC \NXKs)'^Kg\~2}6}Գ)n]Or^j~"{p29w6/.z-v:+M{WJYZ굢`% Ҥl9ힶկ#OUz+U?;sd~vND7*.Y+v:ye;8}~|+ÑޅN9}{Bƞ#txխsXɿkSV/uJ=o G<ջL'L:D]6jfgLz/+ؽ[{rCMYq~[{yy czA;w9zszWHVax3 ף2iTXtXML:com.adobe.xmp + + + + Adobe Fireworks CS6 (Macintosh) + 2012-11-14T19:20:21Z + 2012-11-14T19:22:55Z + + + image/png + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +TIDAT8cdVZƈM .Kj_qܹ +bm.5u1000L2%lrrrF\J[RzL1^(IENDB` \ No newline at end of file diff --git a/core/modules/layout/includes/layout.admin.inc b/core/modules/layout/includes/layout.admin.inc new file mode 100644 index 0000000..489e8a3 --- /dev/null +++ b/core/modules/layout/includes/layout.admin.inc @@ -0,0 +1,195 @@ +getDefinitions(); + + $rows = array(); + $header = array(t('Name'), t('Source')); + foreach ($layouts as $name => $layout) { + $provider_info = system_get_info($layout['provider']['type'], $layout['provider']['provider']); + + // Build table columns for this row. + $row = array(); + $row['name'] = l($layout['title'], 'admin/structure/templates/manage/' . $name); + // Type can either be 'module' or 'theme'. + $row['provider'] = t('@name @type', array('@name' => $provider_info['name'], '@type' => t($layout['provider']['type']))); + + $rows[] = $row; + } + + $build = array(); + $build['table'] = array( + '#theme' => 'table', + '#header' => $header, + '#rows' => $rows, + ); + return $build; + + // Ensure the provider types are translatable. These do not need to run, + // just inform the static code parser of these source strings. + t('module'); + t('theme'); +} + +/** + * Page callback: Demonstrates a layout template. + * + * @param string $key + * The key of the page layout being requested. + * + * @return array + * An array as expected by drupal_render(). + * + * @see layout_menu() + */ +function layout_page_view($key) { + $layout = layout_manager()->getDefinition($key); + drupal_set_title(t('View template %name', array('%name' => $layout['title'])), PASS_THROUGH); + + // Render the layout in an admin context with region demonstrations. + $instance = layout_manager()->createInstance($key, array()); + $regions = $instance->getRegions(); + foreach ($regions as $region => $info) { + $regions[$region] = '
' . check_plain($info['label']) . '
'; + } + $build['demonstration'] = array( + '#type' => 'markup', + '#markup' => $instance->renderLayout(TRUE, $regions), + ); + $build['#attached']['css'][] = drupal_get_path('module', 'layout') . '/layout.admin.css'; + return $build; +} + +/** + * Page callback: Presents list of displays. + * + * @see display_menu() + */ +function layout_master_list() { + $controller = entity_list_controller('display'); + return $controller->render(); +} + +/** + * Page callback: Presents the display editing form. + * + * @see display_menu() + */ +function layout_master_edit(Display $display) { + drupal_set_title(t('Edit layout @label', array('@label' => $display->label())), PASS_THROUGH); + return entity_get_form($display); +} + +/** + * Page callback: webservice handling RESTful(-ish) requests from the JS app. + * Currently it only updates the *whole* + * + * @todo: convert this to use the routing system (currently we use + * Backbone.emulateHTTP). + * @todo: allow this to consume application/json-payloads (currently we need + * Backbone.emulateJSON) + * @todo: protect against CSRF. We should use a session-based token. This issue + * will reoccur with all RESTful services, because a per-request token (à la + * form-token just is not RESTful). + * + * @param Drupal\layout\Plugin\Core\Entity\Display $display + * @param null $a + * @param null $b + * @param null $c + * @return int + */ +function layout_master_webservice(Display $display, $a=NULL, $b=NULL, $c=NULL) { + if (isset($display->locked) && is_object($display->locked) && $display->locked->owner != $GLOBALS['user']->uid) { + // @todo: currently menu_access_denied returns a status 200? + // @todo: make sure JS client handles this properly. + return MENU_ACCESS_DENIED; + } + // This is all evil, evil, evil - hacking this blindly bottom-up. + $payload = isset($_POST['model']) ? drupal_json_decode($_POST['model']) : FALSE; + $method = isset($_POST['_method']) ? $_POST['_method'] : FALSE; + switch ($a) { + // stores complete layout + case 'layout': + $blockInfo = array(); + // Set payload - this *obviously* needs to be validated. + foreach ($payload['regions'] as $region) { + foreach ($region['blockInstances'] as $blockInstance) { + $block = $blockInstance['id']; + $blockInfo['block.' . $block] = array( + 'region' => $region['id'], + 'weight' => $blockInstance['weight'] * 100 + ); + } + } + $display->set('blockInfo', $blockInfo); + // Store changes in TempStore. + layout_master_cache_set($display); + drupal_exit(); + break; + // Update single region + case 'region': + $region_id = $b; + die(); + break; + // Update single block + case 'block': + $block_id = $b; + break; + } + return ; +} + + +/** + * Page to break lock on a display being edited. + */ +function layout_master_break_lock_confirm($form, &$form_state, Display $display) { + $form_state['display'] = &$display; + $form = array(); + if (empty($display->locked)) { + $form['message']['#markup'] = t('There is no lock on display %name to break.', array('%name' => $display->get('id'))); + return $form; + } + + $cancel = drupal_container()->get('request')->query->get('cancel'); + if (empty($cancel)) { + $cancel = 'admin/structure/layouts/manage/' . $display->get('id') . '/edit'; + } + + $account = user_load($display->locked->owner); + $form = confirm_form($form, + t('Do you want to break the lock on display %name?', + array('%name' => $display->get('label'))), + $cancel, + t('By breaking this lock, any unsaved changes made by !user will be lost.', array('!user' => theme('username', array('account' => $account)))), + t('Break lock'), + t('Cancel')); + $form['actions']['submit']['#submit'][] = 'layout_master_break_lock_confirm_submit'; + return $form; +} + +/** + * Form submit handler to break_lock on a display. + */ +function layout_master_break_lock_confirm_submit(&$form, &$form_state) { + $display = $form_state['display']; + drupal_container()->get('user.tempstore')->get('layout')->delete($display->get('id')); + $form_state['redirect'] = 'admin/structure/layouts/manage/' . $display->get('id') . '/edit'; + drupal_set_message(t('The lock has been broken and you may now edit this display.')); +} diff --git a/core/modules/layout/js/collections/collections.js b/core/modules/layout/js/collections/collections.js new file mode 100644 index 0000000..c41a80a --- /dev/null +++ b/core/modules/layout/js/collections/collections.js @@ -0,0 +1,48 @@ +/** + * @file + * This file contains the collections of models for the layout js-app. + * + * @todo: split into separate files. + */ +(function ($, _, Backbone, Drupal, drupalSettings) { + "use strict"; + + Drupal.layout = Drupal.layout || {}; + + Drupal.layout.RegionsCollection = Backbone.Collection.extend({ + model: Drupal.layout.RegionModel + }); + + Drupal.layout.BlocksCollection = Backbone.Collection.extend({ + model: Drupal.layout.BlockModel + }); + + Drupal.layout.BlockInstancesCollection = Backbone.Collection.extend({ + model: Drupal.layout.BlockInstanceModel, + initialize: function() { + // Reorder every time a block instance is added or removed. + this.on('add', this.reorder, this); + this.on('remove', this.reorder, this); + }, + /** + * Sorting callback for the collection. + * @param {Drupal.layout.BlockInstanceModel} + * @return {Number} + */ + comparator: function(model) { + return model.get('weight'); + }, + /** + * Make sure that weight attribute of the models correspond to their index. + */ + reorder: function(options) { + this.each(function (model, index) { + model.set('weight', index); + }); + if (!options || !options.silent) { + this.trigger('reorder'); + } + } + }); + +})(jQuery, _, Backbone, Drupal, drupalSettings); diff --git a/core/modules/layout/js/layout.admin.js b/core/modules/layout/js/layout.admin.js new file mode 100755 index 0000000..43c1563 --- /dev/null +++ b/core/modules/layout/js/layout.admin.js @@ -0,0 +1,98 @@ +(function ($, window, Drupal, drupalSettings) { + +"use strict"; + +var appView; + +/** + * Attach display editor functionality. + */ +Drupal.behaviors.displayEditor = { + attach: function (context, settings) { + function randomId() { + var chars = "abcdefghiklmnopqrstuvwxyz"; + var randomString = ''; + for (var i=0; i < 8; i++) { + var rnum = Math.floor(Math.random() * chars.length); + randomString += chars.substring(rnum,rnum+1); + } + return randomString; + } + + /** + * Helper function generating a BlocksCollection populated with randomly + * named items. + * + * @return {Drupal.layout.BlocksCollection} + */ + Drupal.layout.getBlocksCollection = function() { + var blocks = []; + // Generate a bunch of randomly named blocks. + for (var i = 0; i<10; i++) { + var id = randomId(); + var b = new Drupal.layout.BlockModel({ + 'id': id, + 'label': 'Label ' + id + }); + blocks.push(b); + } + return new Drupal.layout.BlocksCollection(blocks); + } + + /** + * Generates the required Backbone Collections and Models. + * @param layoutData + * @return {Drupal.layout.RegionsCollection} + */ + function generateRegionCollections(layoutData) { + var regions = new Drupal.layout.RegionsCollection(); + _(layoutData.regions).each(function(region) { + regions.add(new Drupal.layout.RegionModel({ + id: region.id, + label: region.label, + blockInstances: + new Drupal.layout.BlockInstancesCollection().reset(region.blockInstances) + })); + }); + return regions; + } + + // Initial attaching. + if (!appView) { + Drupal.layout.appModel = new Drupal.layout.AppModel({ + id: drupalSettings.layout.id, + layout: drupalSettings.layout.layoutData.layout, + regions: generateRegionCollections(drupalSettings.layout.layoutData) + }); + appView = new Drupal.layout.AppView({ + model: Drupal.layout.appModel, + el: $('#block-system-main'), + locked: drupalSettings.layout.locked + }); + // @todo: we need to do this in order to circumvent the merge-behavior of + // Drupal.ajax on drupalSettings (which makes sense, just not here). + drupalSettings.layout.layoutData = {}; + appView.render(); + } else { + // Drupal.ajax has (good) reasons to call the attach function three times + // per response (triggered by layout select menu). But we + // need this only once and we need to make sure that the layout data is + // replaced not merged, that's why we do this stunt. There needs to be + // some form of making this less awkward. + if (drupalSettings.layout.layoutData.id) { + // Updating the model will trigger an rendering as appropriate. + Drupal.layout.appModel.set({ + id: drupalSettings.layout.id, + layout: drupalSettings.layout.layoutData.layout, + regions: generateRegionCollections(drupalSettings.layout.layoutData) + }); + // @todo: we need to do this in order to circumvent the merge-behavior of + // Drupal.ajax on drupalSettings (which makes sense, just not here). + drupalSettings.layout.layoutData = {}; + } + } + + } +}; + +})(jQuery, window, Drupal, drupalSettings); diff --git a/core/modules/layout/js/models/app-model.js b/core/modules/layout/js/models/app-model.js new file mode 100644 index 0000000..6fd97e7 --- /dev/null +++ b/core/modules/layout/js/models/app-model.js @@ -0,0 +1,24 @@ +/** + * @file + * This model hold application state and corresponds to the layout containing + * regions (and block instances). + * + * @todo: probably split this AppModel into AppModel and LayoutModel. + */ +(function ($, _, Backbone, Drupal) { + "use strict"; + + Drupal.layout = Drupal.layout || {}; + Drupal.layout.AppModel = Backbone.Model.extend({ + url: function() { + return drupalSettings.layout.webserviceURL + '/layout'; + }, + defaults: { + 'id': null, + 'layout': null, + 'regions': null, + 'config': null + } + }); + +})(jQuery, _, Backbone, Drupal); diff --git a/core/modules/layout/js/models/block-model.js b/core/modules/layout/js/models/block-model.js new file mode 100644 index 0000000..e746ef5 --- /dev/null +++ b/core/modules/layout/js/models/block-model.js @@ -0,0 +1,30 @@ +/** + * @file + * This model corresponds to a block that can be placed + */ +(function ($, _, Backbone, Drupal, drupalSettings) { + "use strict"; + + Drupal.layout = Drupal.layout || {}; + + Drupal.layout.BlockModel = Backbone.Model.extend({ + defaults: { + /* CMI name */ + 'id': null, + 'label': '', + 'description': '', + 'config': {} + }, + // @todo: create a new BlockInstanceModel instance via a webservice (or + // another way of ensuring that the id of the BlockInstanceMdoel is unique). + createBlockInstance: function() { + return new Drupal.layout.BlockInstanceModel({ + 'id': this.get('id'), + 'label': this.get('id'), + 'blockId': this.get('id'), + 'config': this.get('config') + }); + } + }); + +})(jQuery, _, Backbone, Drupal, drupalSettings); diff --git a/core/modules/layout/js/models/blockinstance-model.js b/core/modules/layout/js/models/blockinstance-model.js new file mode 100644 index 0000000..d39caf1 --- /dev/null +++ b/core/modules/layout/js/models/blockinstance-model.js @@ -0,0 +1,26 @@ +/** + * @file + * This model corresponds to the instance of a block placed in a region of a + * layout. + */ +(function ($, _, Backbone, Drupal, drupalSettings) { + "use strict"; + + Drupal.layout = Drupal.layout || {}; + + Drupal.layout.BlockInstanceModel = Backbone.Model.extend({ + url: function() { + return drupalSettings.layout.webserviceURL + '/block'; + }, + defaults: { + // Unique id of the block instance. + 'id': null, + 'weight': null, + // Unique id of the block (e.g CMI key). + 'blockId': null, + 'region': '', + 'config': {} + } + }); + +})(jQuery, _, Backbone, Drupal, drupalSettings); diff --git a/core/modules/layout/js/models/region-model.js b/core/modules/layout/js/models/region-model.js new file mode 100644 index 0000000..0cb35e3 --- /dev/null +++ b/core/modules/layout/js/models/region-model.js @@ -0,0 +1,21 @@ +/** + * @file + * This model corresponds to a region in a layout. + */ +(function ($, _, Backbone, Drupal) { + "use strict"; + + Drupal.layout = Drupal.layout || {}; + + Drupal.layout.RegionModel = Backbone.Model.extend({ + url: function() { + return drupalSettings.layout.webserviceURL + '/region/' + this.get('id'); + }, + defaults: { + 'id': null, + 'blockInstances': null, + 'config': null + } + }); + +})(jQuery, _, Backbone, Drupal); diff --git a/core/modules/layout/js/theme.js b/core/modules/layout/js/theme.js new file mode 100644 index 0000000..b0e24b1 --- /dev/null +++ b/core/modules/layout/js/theme.js @@ -0,0 +1,49 @@ +/** + * @file + * Theme functions for the layout js-app. + */ +(function ($) { + /** + * Theme function for a region. + * @param id + * @param label + * @return {String} + */ + Drupal.theme.layoutRegion = function (id, label) { + var html = + '
' + + '
' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + label + '
' + + '
' + + '
' + + '
' + '
' + + '
' + + '
'; + return html; + } + + /** + * Theme function to get the html for a block instance. + * @param id + * @param label + * @return {String} + */ + Drupal.theme.layoutBlock = function (id, label, attributes) { + return '
' + + '
' + + '
' + + 'M' + + '' + id + ' block' + + '
' + + '
' + + '
' + + '
'; + }; + +})(jQuery); diff --git a/core/modules/layout/js/views/app-view.js b/core/modules/layout/js/views/app-view.js new file mode 100644 index 0000000..b9c2814 --- /dev/null +++ b/core/modules/layout/js/views/app-view.js @@ -0,0 +1,53 @@ +/** + * @file + * This file holds the master view for the layout js app. + */ +(function ($, _, Backbone, Drupal) { + + "use strict"; + + // @todo: be RESTful. Not sure whether the router/rest.module are in place. + // Instead of using REST verbs, POST will be used and $_POST['_method'] will + // contain the verb. + Backbone.emulateHTTP = true; + // Payloads will be sent as application/x-www-form-urlencoded, in + // $_POST['model']. + Backbone.emulateJSON = true; + + Drupal.layout = Drupal.layout || {}; + Drupal.layout.AppView = Backbone.View.extend({ + initializeRegions: function() { + this.regionsView = new Drupal.layout.UpdatingCollectionView({ + el: this.$el.find('.layout-display'), + collection: this.model.get('regions'), + nestedViewConstructor:Drupal.layout.RegionView, + nestedViewTagName:'div' + }); + }, + initialize: function(options) { + this.initializeRegions(); + // Listen to changes of the layout-property for a complete repaint. + this.model.on('change:layout', function(m) { + this.remove(); + // Reinitialize region - @todo: find a way of doing this w/o + // reinitializing the view. + this.initializeRegions(); + this.render(); + }, this); + }, + render: function() { + // @todo: this should move to layout.admin.js and provide better handling. + // Do not setup the js app if another user is currently operating on this + // layout (locked on the server via TempStore). + if (this.options.locked) { + return false; + } + this.regionsView.render(); + return this; + }, + remove: function() { + this.regionsView.remove(); + } + }); + +})(jQuery, _, Backbone, Drupal); diff --git a/core/modules/layout/js/views/blockinstance-view.js b/core/modules/layout/js/views/blockinstance-view.js new file mode 100644 index 0000000..aded8a8 --- /dev/null +++ b/core/modules/layout/js/views/blockinstance-view.js @@ -0,0 +1,38 @@ +/** + * @file + * This view controls a single BlockInstance. + */ +(function ($, _, Backbone, Drupal) { + + "use strict"; + + Drupal.layout = Drupal.layout || {}; + + Drupal.layout.BlockInstanceView = Backbone.View.extend({ + events:{ + 'click':'onClick', + 'drop':'onDrop' + }, + onDrop:function (event, index) { + // Trigger reorder, will be handled in Drupal.layout.RegionView. + this.$el.trigger('reorder', [this.model, index]); + // @todo: handle dropping onto enpty region. + event.preventDefault(); + event.stopPropagation(); + return ; + }, + onClick:function () { + this.dialogView = new Drupal.layout.BlockInstanceModalView({ + model: this.model, + title: this.model.get('label') + }); + this.dialogView.render(); + }, + render:function () { + this.setElement($(Drupal.theme('layoutBlock', this.model.get('id'), this.model.get('label'), this.model.attributes))); + return this; + } + }); + + +})(jQuery, _, Backbone, Drupal); diff --git a/core/modules/layout/js/views/blockinstancemodal-view.js b/core/modules/layout/js/views/blockinstancemodal-view.js new file mode 100644 index 0000000..7746d03 --- /dev/null +++ b/core/modules/layout/js/views/blockinstancemodal-view.js @@ -0,0 +1,34 @@ +/** + * @file + * This view controls and instantiates the dialog configuring a BlockInstance. + */ +(function ($, _, Backbone, Drupal) { + + "use strict"; + + Drupal.layout = Drupal.layout || {}; + Drupal.layout.BlockInstanceModalView = Drupal.layout.ModalView.extend({ + events: { + 'click button.delete': 'removeBlockInstance' + }, + removeBlockInstance: function() { + // Remove model. + this.model.collection.remove(this.model); + // Destroy model on server. + this.model.destroy(); + // Close and remove dialog. + this.remove(); + }, + render: function() { + this.$el.html( + ' ' + + '' + ); + this.show(); + return this; + } + }); + +})(jQuery, _, Backbone, Drupal); + + diff --git a/core/modules/layout/js/views/blockselectormodal-view.js b/core/modules/layout/js/views/blockselectormodal-view.js new file mode 100644 index 0000000..ea2c600 --- /dev/null +++ b/core/modules/layout/js/views/blockselectormodal-view.js @@ -0,0 +1,69 @@ +/** + * @file + * BlockSelectorModalView displays Blocks available for placement in a given + * region, creates BlockInstance from a selected Block adds it to the region. + * + * @todo: use a form/server-side generated listing or a proper webservice to + * retrieve the "placeable" Blocks. + * @todo: split of Drupal.layout.BlockListItemView into separate file. + */ +(function ($, _, Backbone, Drupal) { + + "use strict"; + + Drupal.layout = Drupal.layout || {}; + Drupal.layout.BlockSelectorModalView = Drupal.layout.ModalView.extend({ + events: { + 'select': 'selectBlock' + }, + selectBlock: function(e, block) { + // Model is RegionModel + var instance = block.createBlockInstance(); + // Append at the end. + instance.set('weight', this.model.get('blockInstances').length); + this.model.get('blockInstances').add(instance); + // Remove & close dialog. + this.remove(); + }, + tagName: 'ul', + render: function() { + this.$el.empty(); + // @todo: refactor this to use nestedViewContainerSelector. + this._collectionView = new Drupal.layout.UpdatingCollectionView({ + collection:this.collection, + nestedViewConstructor:Drupal.layout.BlockListItemView, + nestedViewTagName:'li' + }); + this._collectionView.setElement(this.$el); + this._collectionView.render(); + this.show(); + return this; + }, + remove: function() { + if (this._collectionView) { + this._collectionView.remove(); + } + // Apparently no need to call this.dialog.close(); remove this.$el + // closes the jQueryUI Dialog, oh jqueryui magic ... + this.$el.remove(); + } + }); + + Drupal.layout.BlockListItemView = Backbone.View.extend({ + events: { + 'click a': 'selectBlock' + }, + selectBlock: function(e) { + // Pass this click & model to the parent view. + this.$el.trigger('select', [this.model]); + e.preventDefault(); + e.stopPropagation(); + return ; + }, + render: function() { + this.$el.html('Add Block ' + this.model.get('label') + ''); + return this; + } + }); + +})(jQuery, _, Backbone, Drupal); diff --git a/core/modules/layout/js/views/modal-view.js b/core/modules/layout/js/views/modal-view.js new file mode 100644 index 0000000..26eba39 --- /dev/null +++ b/core/modules/layout/js/views/modal-view.js @@ -0,0 +1,33 @@ +/** + * @file + * Base view wrapping a dialog.js based dialog. + * + * @todo: maybe provide meaningful form-loading? + */ +(function ($, _, Backbone, Drupal) { + + "use strict"; + + Drupal.layout = Drupal.layout || {}; + + Drupal.layout.ModalView = Backbone.View.extend({ + dialog: null, + callback: null, + initialize: function(options) { + this.callback = options.callback || null; + this.dialog = Drupal.dialog(this.$el, {title: this.options.title}); + }, + show: function() { + this.dialog.showModal(); + }, + close: function() { + this.dialog.close(); + }, + remove: function() { + // Apparently no need to call this.dialog.close(); remove this.$el + // closes the jQueryUI Dialog, oh jqueryui magic ... + this.$el.remove(); + } + }); + +})(jQuery, _, Backbone, Drupal); diff --git a/core/modules/layout/js/views/region-view.js b/core/modules/layout/js/views/region-view.js new file mode 100644 index 0000000..d5d1995 --- /dev/null +++ b/core/modules/layout/js/views/region-view.js @@ -0,0 +1,108 @@ +/** + * @file + * This view controls a region and the blockInstances contained in it. Opens + * the BlockSelectorModalView on request. + */ +(function ($, _, Backbone, Drupal) { + + "use strict"; + + Drupal.layout = Drupal.layout || {}; + + Drupal.layout.RegionView = Backbone.View.extend({ + events:{ + 'click [name="block"][value="add"]':'onClickAdd', + 'click [name="block"][value="configure"]':'onClickConfigure', + 'reorder':'reorderInstances' + }, + + onClickConfigure:function (e) { + this.dialogView = new Drupal.layout.RegionModalView({ + model: this.model, + title: this.model.get('label') + }); + this.dialogView.render(); + e.preventDefault(); + e.stopPropagation(); + }, + + onClickAdd:function (e) { + var collection = Drupal.layout.getBlocksCollection(); + this.modalView = new Drupal.layout.BlockSelectorModalView({ + model: this.model, + collection: collection, + title: Drupal.t('Please select a block to place in the regions %region', {'%region': this.model.get('label')}) + }); + this.modalView.render(); + e.preventDefault(); + e.stopPropagation(); + return ; + }, + + // @todo: listen to collection events in app-view.js instead/propagate event. + saveFullLayout: function() { + // Show the "changed" notice. + $('.display-changed').removeClass('js-hide'); + Drupal.layout.appModel.save(); + }, + + initialize:function () { + var blockInstances = this.model.get('blockInstances'); + this._collectionView = new Drupal.layout.UpdatingCollectionView({ + collection:blockInstances, + nestedViewConstructor:Drupal.layout.BlockInstanceView, + nestedViewTagName:'div', + el: this.$el, + nestedViewContainerSelector: '.blocks .row' + }); + + // @todo: be more selective about what changes trigger requests to the + // server. And let that bubble up to the app-view or only persist the + // region-specific changes here. + blockInstances.on('reorder', this.saveFullLayout, this); + blockInstances.on('add', this.saveFullLayout, this); + blockInstances.on('remove', this.saveFullLayout, this); + }, + + render:function () { + this.$el.html(Drupal.theme.layoutRegion(this.model.get('id'), this.model.get('label'))); + this._collectionView.render(); + // Making the whole layout-region-element sortable provides a larger area + // to drop block instances on and allows for dropping on empty regions. + this.$('.layout-region').sortable({ + items: '.block', + connectWith: '.layout-region', + cursor: 'move', + stop: function(event, ui) { + ui.item.trigger('drop', ui.item.index()); + } + }); + return this; + }, + + remove:function () { + this.$el.sortable('destroy'); + this.$el.empty(); + this._collectionView.remove(); + }, + + reorderInstances:function (event, model, position) { + var collection = this.model.get('blockInstances'); + var originCollection; + // Handle cross-collection drag and drop. + if (!collection.contains(model)) { + originCollection = model.collection; + // Let's remove it from the other first before adding it here. + model.collection.remove(model, {silent: true}); + // This is set to silent to avoid potential race condition. + originCollection.reorder({silent: true}); + } else { + // We'll be re-adding immediately, so no need for rapid-fire events. + collection.remove(model, {silent: true}); + } + collection.add(model, {at:position}); + this.render(); + } + }); + +})(jQuery, _, Backbone, Drupal); diff --git a/core/modules/layout/js/views/regionmodal-view.js b/core/modules/layout/js/views/regionmodal-view.js new file mode 100644 index 0000000..4eca478 --- /dev/null +++ b/core/modules/layout/js/views/regionmodal-view.js @@ -0,0 +1,23 @@ +/** + * @file + */ +(function ($, _, Backbone, Drupal) { + + "use strict"; + + Drupal.layout = Drupal.layout || {}; + Drupal.layout.RegionModalView = Drupal.layout.ModalView.extend({ + events: { + }, + render: function() { + this.$el.html( + '
Configure region type? Load some FAPI form here.
' + + '' + ); + this.show(); + return this; + } + }); + +})(jQuery, _, Backbone, Drupal); + diff --git a/core/modules/layout/js/views/updatingcollection-view.js b/core/modules/layout/js/views/updatingcollection-view.js new file mode 100644 index 0000000..fb42ebf --- /dev/null +++ b/core/modules/layout/js/views/updatingcollection-view.js @@ -0,0 +1,121 @@ +/** + * @file + */ +(function ($, _, Backbone, Drupal) { + "use strict"; + + Drupal.layout = Drupal.layout || {}; + + Drupal.layout.UpdatingCollectionView = Backbone.View.extend({ + initialize:function (options) { + if (!options.nestedViewConstructor) { + throw "no child view constructor provided"; + } + if (!options.nestedViewTagName) { + throw "no child view tag name provided"; + } + + this._nestedViews = []; + this.collection.each(this._addModel, this); + this.collection.bind('add', this._addModel, this); + this.collection.bind('remove', this._removeModel, this); + }, + + /** + * Retrieves a nested Backbone.View by its Backbone.Model + * @param model + * @return {Backbone.Model} + * @private + */ + _getViewByModel: function(model) { + // @todo this probably should be cached/tracked. + var vs = _(this._nestedViews).select(function (nv) { + return nv.model === model; + }); + return vs.length ? vs[0] : false; + }, + + /** + * Return either this.$el or if a nestedViewContainerSelector-option was + * given the element that matches this.$(nestedViewContainerSelector). + * + * @return {jQuery} + * @private + */ + _getContainerElement: function() { + if (this.options.nestedViewContainerSelector) { + return this.$(this.options.nestedViewContainerSelector); + } + else { + return this.$el; + } + }, + + /** + * Called when a new model is added to the collection. + * + * @param {Backbone.Model} model + * @private + */ + _addModel:function (model) { + var nv = new this.options.nestedViewConstructor({ + tagName:this.options.nestedViewTagName, + model:model + }); + + this._nestedViews.push(nv); + if (this._rendered) { + this._getContainerElement().append(nv.render().$el); + } + }, + + /** + * Called when a new model is removed from the collection. + * + * @param {Backbone.Model} model + * @private + */ + _removeModel:function (model) { + var viewToRemove = this._getViewByModel(model); + this._nestedViews = _(this._nestedViews).without(viewToRemove); + if (this._rendered && viewToRemove) { + viewToRemove.$el.remove(); + } + }, + + /** + * Renders all nested views (one per model in the view's collection). + * + * @return {Drupal.layout.UpdatingCollectionView} + */ + render:function () { + this._rendered = true; + var $el = this._getContainerElement(); + $el.empty(); + // Use the collection to make sure the order of the rendered views is + // up-to-date. + this.collection.each(function(m) { + var nv = this._getViewByModel(m); + // Check that a view could be retrieved. + if (nv) { + $el.append(nv.render().$el); + } + }, this); + return this; + }, + + /** + * Remove all nested views. + * @todo: should we instead remove the models from the collection? Currently + * we leave the collection intact but retrieve each nested view and remove it. + */ + remove: function() { + // Cleanup. + this.collection.each(function(m) { + this._removeModel(m); + }, this); + this._getContainerElement().remove(); + } + }); + +})(jQuery, _, Backbone, Drupal); diff --git a/core/modules/layout/layout.admin.css b/core/modules/layout/layout.admin.css deleted file mode 100644 index 6e38bf4..0000000 --- a/core/modules/layout/layout.admin.css +++ /dev/null @@ -1,17 +0,0 @@ -.layout-display { - background: rgb(224, 224, 224); -} - -.layout-region-demonstration { - background-image: -moz-linear-gradient(bottom, rgb(70,70,71) 40%, rgb(91,91,94) 70%, rgb(125,124,125) 88%); - background-image: -o-linear-gradient(bottom, rgb(70,70,71) 40%, rgb(91,91,94) 70%, rgb(125,124,125) 88%); - background-image: -ms-linear-gradient(bottom, rgb(70,70,71) 40%, rgb(91,91,94) 70%, rgb(125,124,125) 88%); - background-image: -webkit-linear-gradient(bottom, rgb(70,70,71) 40%, rgb(91,91,94) 70%, rgb(125,124,125) 88%); - background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0.4, rgb(70,70,71)), color-stop(0.7, rgb(91,91,94)), color-stop(0.88, rgb(125,124,125))); - background-image: linear-gradient(bottom, rgb(70,70,71) 40%, rgb(91,91,94) 70%, rgb(125,124,125) 88%); - color: white; - font-size: 0.8em; - margin: 3px; - padding: 10px; - text-transform: uppercase; -} diff --git a/core/modules/layout/layout.admin.inc b/core/modules/layout/layout.admin.inc deleted file mode 100644 index 9a1a921..0000000 --- a/core/modules/layout/layout.admin.inc +++ /dev/null @@ -1,75 +0,0 @@ -getDefinitions(); - - $rows = array(); - $header = array(t('Name'), t('Source')); - foreach ($layouts as $name => $layout) { - $provider_info = system_get_info($layout['provider']['type'], $layout['provider']['provider']); - - // Build table columns for this row. - $row = array(); - $row['name'] = l($layout['title'], 'admin/structure/templates/manage/' . $name); - // Type can either be 'module' or 'theme'. - $row['provider'] = t('@name @type', array('@name' => $provider_info['name'], '@type' => t($layout['provider']['type']))); - - $rows[] = $row; - } - - $build = array(); - $build['table'] = array( - '#theme' => 'table', - '#header' => $header, - '#rows' => $rows, - ); - return $build; - - // Ensure the provider types are translatable. These do not need to run, - // just inform the static code parser of these source strings. - t('module'); - t('theme'); -} - -/** - * Page callback: Demonstrates a layout template. - * - * @param string $key - * The key of the page layout being requested. - * - * @return array - * An array as expected by drupal_render(). - * - * @see layout_menu() - */ -function layout_page_view($key) { - $layout = layout_manager()->getDefinition($key); - drupal_set_title(t('View template %name', array('%name' => $layout['title'])), PASS_THROUGH); - - // Render the layout in an admin context with region demonstrations. - $instance = layout_manager()->createInstance($key, array()); - $regions = $instance->getRegions(); - foreach ($regions as $region => $info) { - $regions[$region] = '
' . check_plain($info['label']) . '
'; - } - $build['demonstration'] = array( - '#type' => 'markup', - '#markup' => $instance->renderLayout(TRUE, $regions), - ); - $build['#attached']['css'][] = drupal_get_path('module', 'layout') . '/layout.admin.css'; - return $build; -} diff --git a/core/modules/layout/layout.module b/core/modules/layout/layout.module old mode 100644 new mode 100755 index c6ed7ae..73861d4 --- a/core/modules/layout/layout.module +++ b/core/modules/layout/layout.module @@ -5,17 +5,20 @@ * Manages page layouts for content presentation. */ +use Drupal\layout\Plugin\Core\Entity\Display; + /** * Implements hook_menu(). */ function layout_menu() { + // Layout template demonstration. $items['admin/structure/templates'] = array( 'title' => 'Templates', 'description' => 'Overview of the list of layout templates available.', 'page callback' => 'layout_page_list', 'access callback' => 'user_access', 'access arguments' => array('administer layouts'), - 'file' => 'layout.admin.inc', + 'file' => 'includes/layout.admin.inc', ); $items['admin/structure/templates/manage/%'] = array( 'title' => 'View template', @@ -23,8 +26,44 @@ function layout_menu() { 'page arguments' => array(4), 'access callback' => 'layout_user_access', 'access arguments' => array(4), - 'file' => 'layout.admin.inc', + 'file' => 'includes/layout.admin.inc', + ); + // Master layout editing. + $items['admin/structure/layouts'] = array( + 'title' => 'Layouts', + 'description' => 'Overview of the list of layouts available.', + 'page callback' => 'layout_master_list', + 'access callback' => 'user_access', + 'access arguments' => array('administer layouts'), + 'file' => 'includes/layout.admin.inc', + ); + $items['admin/structure/layouts/manage/%layout_master_cache'] = array( + 'title' => 'Edit layout', + 'page callback' => 'layout_master_edit', + 'page arguments' => array(4), + 'access callback' => 'user_access', + 'access arguments' => array('administer layouts'), + 'file' => 'includes/layout.admin.inc', + ); + + $items['admin/structure/layouts/manage/%layout_master_cache/webservice'] = array( + 'title' => 'Webservice', + 'page callback' => 'layout_master_webservice', + 'page arguments' => array(4), + 'access callback' => 'user_access', + 'access arguments' => array('administer layouts'), + 'file' => 'includes/layout.admin.inc', ); + + $items['admin/structure/layouts/manage/%layout_master_cache/break-lock'] = array( + 'title' => 'Break lock', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('layout_master_break_lock_confirm', 4), + 'access callback' => 'user_access', + 'access arguments' => array('administer layouts'), + 'file' => 'includes/layout.admin.inc', + ); + return $items; } @@ -80,3 +119,137 @@ function layout_theme($existing, $type, $theme, $path) { } return $items; } + +/** + * Entity URI callback. + * + * @param \Drupal\layout\Plugin\Core\Entity\Display $display + * Dispaly configuration entity instance. + * + * @return array + * Entity URI information. + */ +function layout_master_uri(Display $display) { + return array( + 'path' => 'admin/structure/layouts/manage/' . $display->id(), + ); +} + +/** + * Load one display object by its identifier. + * + * @todo Remove once not needed for layout_menu(). + * + * @return \Drupal\layout\Plugin\Core\Entity\Display + * Display configuration entity instance. + */ +function layout_master_load($id) { + return entity_load('display', $id); +} + +/** + * Specialized menu callback to load a display. + * + * @param $name + * The machine name of the display. + * + * @return + * The display object. + */ +function layout_master_cache_load($name) { + $display_temp_store = drupal_container()->get('user.tempstore')->get('layout'); + $display = $display_temp_store->get($name); + + if (empty($display)) { + $display = layout_master_load($name); + } + + if (empty($display)) { + return FALSE; + } + // This mimics views_ui's behaviour. + $display->editing = TRUE; + $display->locked = $display_temp_store->getMetadata($display->get('id')); + + return $display; +} + +/** + * Specialized cache function. + */ +function layout_master_cache_set(Display $display) { + // This mimics views_ui's behaviour. + $display->changed = TRUE; + drupal_container()->get('user.tempstore')->get('layout')->set($display->id, $display); +} + +/** + * Implements hook_entity_info_alter(). + * + * Add URI callback to Display config entities to support listing API. + */ +function layout_entity_info_alter(&$entity_info) { + $entity_info['display']['uri_callback'] = 'layout_master_uri'; + $entity_info['display']['list_controller_class'] = 'Drupal\layout\DisplayListController'; + $entity_info['display']['form_controller_class']['default'] = 'Drupal\layout\DisplayFormController'; + $entity_info['display']['list_path'] = 'admin/structure/layouts'; + $entity_info['display']['entity_keys']['label'] = 'label'; +} + +/** + * Ajax callback for block placement display switching. + */ +function layout_ajax_block_placement_callback($form, &$form_state) { + return $form['blocks']; +} + +/** + * Implements hook_library_info(). + */ +function layout_library_info() { + $libraries = array(); + $path = drupal_get_path('module', 'layout'); + $libraries['layout.admin'] = array( + 'title' => 'Layout admin', + 'version' => NULL, + 'css' => array( + $path . '/css/layout.base.css' => array(), + $path . '/css/layout.theme.css' => array(), + $path . '/css/layout.icons.css' => array(), + ), + 'js' => array( + // Drupal's pseudo-templates + $path . '/js/theme.js' => array('defer' => TRUE), + + // Models + $path . '/js/layout.admin.js' => array('defer' => TRUE), + $path . '/js/models/app-model.js' => array('defer' => TRUE), + $path . '/js/models/block-model.js' => array('defer' => TRUE), + $path . '/js/models/blockinstance-model.js' => array('defer' => TRUE), + $path . '/js/models/region-model.js' => array('defer' => TRUE), + + // Collections bundled for the time being. + $path . '/js/collections/collections.js' => array('defer' => TRUE), + + // Views other Views extend first + $path . '/js/views/updatingcollection-view.js' => array('defer' => TRUE), + $path . '/js/views/modal-view.js' => array('defer' => TRUE), + + $path . '/js/views/region-view.js' => array('defer' => TRUE), + $path . '/js/views/blockinstance-view.js' => array('defer' => TRUE), + $path . '/js/views/blockinstancemodal-view.js' => array('defer' => TRUE), + + $path . '/js/views/regionmodal-view.js' => array('defer' => TRUE), + $path . '/js/views/blockinstancemodal-view.js' => array('defer' => TRUE), + $path . '/js/views/blockselectormodal-view.js' => array('defer' => TRUE), + + $path . '/js/views/app-view.js' => array('defer' => TRUE), + ), + 'dependencies' => array( + array('system', 'backbone'), + array('system', 'drupal.dialog'), + array('system', 'jquery.ui.sortable'), + ), + ); + return $libraries; +} diff --git a/core/modules/layout/layouts/static/one-col/one-col.tpl.php b/core/modules/layout/layouts/static/one-col/one-col.tpl.php index 61af832..529eb06 100644 --- a/core/modules/layout/layouts/static/one-col/one-col.tpl.php +++ b/core/modules/layout/layouts/static/one-col/one-col.tpl.php @@ -12,7 +12,7 @@ */ ?>
> -
+
diff --git a/core/modules/layout/layouts/static/twocol/two-col.tpl.php b/core/modules/layout/layouts/static/twocol/two-col.tpl.php index 810a052..ab59cac 100644 --- a/core/modules/layout/layouts/static/twocol/two-col.tpl.php +++ b/core/modules/layout/layouts/static/twocol/two-col.tpl.php @@ -14,11 +14,11 @@ */ ?>
> -
+
-
+
diff --git a/core/modules/layout/lib/Drupal/layout/Config/DisplayBase.php b/core/modules/layout/lib/Drupal/layout/Config/DisplayBase.php index afe3229..e40a6c0 100644 --- a/core/modules/layout/lib/Drupal/layout/Config/DisplayBase.php +++ b/core/modules/layout/lib/Drupal/layout/Config/DisplayBase.php @@ -107,7 +107,7 @@ public function mapBlocksToLayout(LayoutInterface $layout) { // No need to do anything. } // Then, try to remap using region types. - else if (!empty($types[$info['region-type']])) { + else if (isset($types[$info['region-type']]) && !empty($types[$info['region-type']])) { $info['region'] = reset($types[$info['region-type']]); } // Finally, fall back to dumping everything in the layout's first region. @@ -135,4 +135,22 @@ public function getAllRegionTypes() { } return array_unique($types); } + + /** + * Implements DisplayInterface::export(). + */ + public function export() { + // Start by exporting the public properties. + $class_info = new \ReflectionClass($this); + $properties = array(); + foreach ($class_info->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) { + $name = $property->getName(); + $properties[$name] = $this->$name; + } + + // Now, add protected data - the blockInfo array. + $properties['blockInfo'] = $this->blockInfo; + + return $properties; + } } diff --git a/core/modules/layout/lib/Drupal/layout/Config/DisplayInterface.php b/core/modules/layout/lib/Drupal/layout/Config/DisplayInterface.php index 871aadc..c40e699 100644 --- a/core/modules/layout/lib/Drupal/layout/Config/DisplayInterface.php +++ b/core/modules/layout/lib/Drupal/layout/Config/DisplayInterface.php @@ -50,7 +50,7 @@ * @return array * An array keyed on each block's configuration object name. Each value is * an array of information that determines the placement of the block within - * a layout, including: + * a layout, including but not limited to: * - region: The region in which to display the block (for bound displays * only). * - region-type: The type of region that is most appropriate for the block. @@ -82,4 +82,11 @@ public function mapBlocksToLayout(LayoutInterface $layout); * region types were assigned. */ public function getAllRegionTypes(); + + /** + * Returns this display as an array suitable for permanent storage. + * + * @return array + */ + public function export(); } diff --git a/core/modules/layout/lib/Drupal/layout/Config/DisplayStorageController.php b/core/modules/layout/lib/Drupal/layout/Config/DisplayStorageController.php new file mode 100644 index 0000000..cc86f93 --- /dev/null +++ b/core/modules/layout/lib/Drupal/layout/Config/DisplayStorageController.php @@ -0,0 +1,25 @@ +export(); + } +} diff --git a/core/modules/layout/lib/Drupal/layout/DisplayFormController.php b/core/modules/layout/lib/Drupal/layout/DisplayFormController.php new file mode 100755 index 0000000..802cacb --- /dev/null +++ b/core/modules/layout/lib/Drupal/layout/DisplayFormController.php @@ -0,0 +1,196 @@ +get('plugin.manager.layout')->getDefinitions(); + $layout_options = array(); + foreach ($layouts as $key => $layout) { + $layout_options[$key] = $layout['title']; + } + $layout_keys = array_keys($layout_options); + + // Add default values to form_state['values']. + if (!isset($form_state['values'])) { + $form_state['values'] = array(); + } + + + $form_state['values'] += array( + 'layout' => isset($display->layout) ? $display->layout : reset($layout_keys) + ); + + $locked = isset($display->locked) && is_object($display->locked) && $display->locked->owner != $GLOBALS['user']->uid; + // Copied from ViewsEditFormController + if ($locked) { + $form['locked'] = array( + '#type' => 'container', + '#attributes' => array('class' => array('view-locked', 'messages', 'warning')), + '#children' => t('This display is being edited by user !user, and is therefore locked from editing by others. This lock is @age old. Click here to break this lock.', + // @todo: add callback to break lock. + array( + '!user' => theme('username', array('account' => user_load($display->locked->owner))), + '@age' => format_interval(REQUEST_TIME - $display->locked->updated), + '@break' => url('admin/structure/layouts/manage/' . $display->get('id') . '/break-lock') + ) + ), + '#weight' => -10, + ); + } + else { + $message = t('* All changes are stored temporarily. Click Save to make your changes permanent. Click Cancel to discard your changes.'); + + $form['changed'] = array( + '#type' => 'container', + '#attributes' => array('class' => array('display-changed', 'messages', 'warning')), + '#children' => $message, + '#weight' => -10, + ); + if (empty($display->changed)) { + $form['changed']['#attributes']['class'][] = 'js-hide'; + } + } + + $form['layout'] = array( + '#type' => 'select', + '#title' => t('Template'), + '#default_value' => $form_state['values']['layout'], + '#options' => $layout_options, + '#ajax' => array( + 'callback' => 'layout_ajax_block_placement_callback', + 'wrapper' => 'display-blocks', + 'method' => 'replace', + 'effect' => 'fade', + ), + ); + + // To support the Ajax interaction, remap the display to the newly selected + // layout. This will reorganize the blocks as appropriate. + if (!isset($display->layout) || ($form_state['values']['layout'] != $display->layout)) { + // @todo: clean this up - this is highly likely to be the wrong place + // to alter the TempStore. + + // But if i *don't* reload the blockInfo property of the Display, it can + // be "stale" and overwrite changes made via the webservice. + $reloaded_display = layout_master_cache_load($display->id); + // Need to copy selectively (because just setting $display = $reloaded_display + // breaks other things). + $display->set('blockInfo', $reloaded_display->get('blockInfo')); + // Now remap. + $layout = layout_manager()->createInstance($form_state['values']['layout'], array()); + $display->remapToLayout($layout); + // Store changes in TempStore. + layout_master_cache_set($display); + } + + // Add block editing interface wrapper for Ajax operation. + $form['blocks'] = array( + '#prefix' => '
', + '#suffix' => '
', + ); + $form['blocks']['demonstration'] = $this->layoutDemonstration($display); + return parent::form($form, $form_state, $display); + } + + /** + * Produces a render array demonstration form of the display. + */ + private function layoutDemonstration(EntityInterface $display) { + // Render the layout in an admin context with region demonstrations. + $layout = layout_manager()->createInstance($display->layout, array()); + $build['demonstration'] = array( + '#type' => 'markup', + '#markup' => $layout->renderLayout(TRUE, array()), + ); + // Add the backbone app. + $build['#attached']['library'][] = array('layout', 'layout.admin'); + $locked = isset($display->locked) && is_object($display->locked) && $display->locked->owner != $GLOBALS['user']->uid; + // Add the webservice URL and display id. + $build['#attached']['js'][] =array( + 'data' => array( + 'layout' => array( + 'webserviceURL' => url('admin/structure/layouts/manage/' . $display->id . '/webservice'), + 'locked' => $locked, + 'layoutData' => $display->exportGroupedByRegion() + ) + ), + 'type' => 'setting', + ); + return $build; + } + + /** + * Overrides Drupal\Core\Entity\EntityFormController::actions(). + */ + protected function actions(array $form, array &$form_state) { + // Only includes a Save action for the entity, no direct Delete button. + return array( + 'submit' => array( + '#value' => t('Save'), + '#validate' => array( + array($this, 'validate'), + ), + '#submit' => array( + array($this, 'submit'), + array($this, 'save'), + ), + ), + 'cancel' => array( + '#value' => t('Cancel'), + '#submit' => array( + array($this, 'cancel'), + ), + ) + ); + } + + /** + * Overrides Drupal\Core\Entity\EntityFormController::save(). + */ + public function save(array $form, array &$form_state) { + $display = $this->getEntity($form_state); + $cached_display = layout_master_cache_load($display->id); + // All changes are already in TempStore. So a save commit all changes. + $cached_display->save(); + // Bust the TempStore. Remove the display form TempStore so it will be reloaded. + drupal_container()->get('user.tempstore')->get('layout')->delete($cached_display->id); + + watchdog('display', 'Layout @label saved.', array('@label' => $display->label()), WATCHDOG_NOTICE); + drupal_set_message(t('Layout %label saved.', array('%label' => $display->label()))); + } + + /** + * Form submission handler for the 'cancel' action. + * + * @param array $form + * An associative array containing the structure of the form. + * @param array $form_state + * A reference to a keyed array containing the current state of the form. + */ + public function cancel(array $form, array &$form_state) { + // Remove this view from cache so edits will be lost. + $display = $this->getEntity($form_state); + $cached_display = layout_master_cache_load($display->id); + drupal_container()->get('user.tempstore')->get('layout')->delete($cached_display->id); + $form_state['redirect'] = 'admin/structure/layouts'; + } + +} diff --git a/core/modules/layout/lib/Drupal/layout/DisplayListController.php b/core/modules/layout/lib/Drupal/layout/DisplayListController.php new file mode 100644 index 0000000..b07e7ee --- /dev/null +++ b/core/modules/layout/lib/Drupal/layout/DisplayListController.php @@ -0,0 +1,63 @@ +uri(); + $operations['edit'] = array( + 'title' => t('Edit'), + 'href' => $uri['path'] . '/edit', + 'options' => $uri['options'], + 'weight' => 10, + ); + return $operations; + } + + /** + * Overrides Drupal\Core\Entity\EntityListController::buildHeader(); + */ + public function buildHeader() { + $row['label'] = t('Layout name'); + $row['applied'] = t('Applied to'); + $row['template'] = t('Template'); + $row['operations'] = t('Operations'); + return $row; + } + + /** + * Overrides Drupal\Core\Entity\EntityListController::buildRow(); + */ + public function buildRow(EntityInterface $entity) { + $row['label'] = check_plain($entity->label()); + // @todo: refer back to pages using this display. + $row['applied'] = $entity->id() == 'front_master' ? t('All front end pages (master)') : t('All admin pages (admin master)'); + $layout = drupal_container()->get('plugin.manager.layout')->getDefinition($entity->layout); + $provider_info = system_get_info($layout['provider']['type'], $layout['provider']['provider']); + // Type can either be 'module' or 'theme'. + $provider_text = t('@name @type', array('@name' => $provider_info['name'], '@type' => t($layout['provider']['type']))); + $row['template'] = check_plain($layout['title'] . ' (' . $provider_text . ')'); + $operations = $this->buildOperations($entity); + $row['operations']['data'] = $operations; + return $row; + } + +} diff --git a/core/modules/layout/lib/Drupal/layout/Plugin/Core/Entity/Display.php b/core/modules/layout/lib/Drupal/layout/Plugin/Core/Entity/Display.php index d954832..7a2daca 100644 --- a/core/modules/layout/lib/Drupal/layout/Plugin/Core/Entity/Display.php +++ b/core/modules/layout/lib/Drupal/layout/Plugin/Core/Entity/Display.php @@ -21,7 +21,7 @@ * id = "display", * label = @Translation("Display"), * module = "layout", - * controller_class = "Drupal\Core\Config\Entity\ConfigStorageController", + * controller_class = "Drupal\layout\Config\DisplayStorageController", * config_prefix = "display.bound", * entity_keys = { * "id" = "id", @@ -117,7 +117,7 @@ protected function sortBlocks() { foreach ($regions as $region_name => &$blocks) { uasort($blocks, 'drupal_sort_weight'); - $this->blocksInRegions[$region_name] = array_keys($blocks); + $this->blocksInRegions[$region_name] = $blocks; } } @@ -183,4 +183,42 @@ public function getLayoutInstance() { return $this->layoutInstance; } + + /** + * Returns an array representation grouped for json-serialisation. + * @todo: this is a bad name (and should probably) + * + * @return array + */ + public function exportGroupedByRegion() { + // Render the layout in an admin context with region demonstrations. + $layout = layout_manager()->createInstance($this->layout, array()); + $regions = $layout->getRegions(); + $data = array( + 'id' => $this->id, + 'layout' => $this->layout + ); + foreach ($regions as $region => $info) { + $region_data = array( + 'id' => $region, + 'label' => $info['label'], + 'blockInstances' => array() + ); + $existing_blocks = $this->getSortedBlocksByRegion($region); + foreach ($existing_blocks as $block => $placement) { + // @todo: this should be proper data. Block instances should maybe + // be classed objects as well. + $block_id = str_replace('block.', '', $block); + $region_data['blockInstances'][] = array( + 'id' => $block_id, + 'label' => $block_id, + 'blockId' => 'default', + 'weight' => $placement['weight'], + 'region' => $placement['region'], + ); + } + $data['regions'][] = $region_data; + } + return $data; + } } diff --git a/core/modules/layout/lib/Drupal/layout/Plugin/Core/Entity/UnboundDisplay.php b/core/modules/layout/lib/Drupal/layout/Plugin/Core/Entity/UnboundDisplay.php index 5450e2c..ed7591b 100644 --- a/core/modules/layout/lib/Drupal/layout/Plugin/Core/Entity/UnboundDisplay.php +++ b/core/modules/layout/lib/Drupal/layout/Plugin/Core/Entity/UnboundDisplay.php @@ -24,7 +24,7 @@ * id = "unbound_display", * label = @Translation("Unbound Display"), * module = "layout", - * controller_class = "Drupal\Core\Config\Entity\ConfigStorageController", + * controller_class = "Drupal\layout\Config\DisplayStorageController", * config_prefix = "display.unbound", * entity_keys = { * "id" = "id", diff --git a/core/modules/layout/lib/Drupal/layout/Tests/DisplayInternalLogicTest.php b/core/modules/layout/lib/Drupal/layout/Tests/DisplayInternalLogicTest.php index 07a77bf..89c2817 100644 --- a/core/modules/layout/lib/Drupal/layout/Tests/DisplayInternalLogicTest.php +++ b/core/modules/layout/lib/Drupal/layout/Tests/DisplayInternalLogicTest.php @@ -67,9 +67,8 @@ public function testBlockSorting() { 'left' => array('block.test_block_3', 'block.test_block_1'), 'right' => array('block.test_block_2'), ); - $this->assertIdentical($this->twocol->getSortedBlocksByRegion('left'), $expected['left']); - $this->assertIdentical($this->twocol->getSortedBlocksByRegion('right'), $expected['right']); - $this->assertIdentical($this->twocol->getAllSortedBlocks(), $expected); + $this->assertIdentical(array_keys($this->twocol->getSortedBlocksByRegion('left')), $expected['left']); + $this->assertIdentical(array_keys($this->twocol->getSortedBlocksByRegion('right')), $expected['right']); } /** @@ -88,7 +87,7 @@ public function testBlockMapping() { ); $two_to_one = clone($this->twocol); $two_to_one->remapToLayout($this->onecol->getLayoutInstance()); - $this->assertIdentical($two_to_one->getAllSortedBlocks(), $expected); + $this->assertIdentical(array_keys($two_to_one->getSortedBlocksByRegion('middle')), $expected['middle']); // Remap from onecol to twocol. Since the blocks are assigned the 'content' // region type, and twocol's 'left' region has that type, the blocks are @@ -99,7 +98,8 @@ public function testBlockMapping() { ); $one_to_two = clone($this->onecol); $one_to_two->remapToLayout($this->twocol->getLayoutInstance()); - $this->assertIdentical($one_to_two->getAllSortedBlocks(), $expected); + $this->assertIdentical(array_keys($one_to_two->getSortedBlocksByRegion('left')), $expected['left']); + $this->assertIdentical(array_keys($one_to_two->getSortedBlocksByRegion('right')), $expected['right']); // Bind the unbound display to the twocol layout: // - Block 1 is assigned the 'content' region type, so is expected to be @@ -115,7 +115,8 @@ public function testBlockMapping() { ); $unbound_to_twocol = $this->unbound->generateDisplay($this->twocol->getLayoutInstance(), 'unbound_to_twocol'); $this->assertTrue($unbound_to_twocol instanceof Display, 'Binding the unbound display successfully created a Display object'); - $this->assertIdentical($unbound_to_twocol->getAllSortedBlocks(), $expected); + $this->assertIdentical(array_keys($unbound_to_twocol->getSortedBlocksByRegion('left')), $expected['left']); + $this->assertIdentical(array_keys($unbound_to_twocol->getSortedBlocksByRegion('right')), $expected['right']); // Generate an unbound display from the twocol display. $expected = array(