1
+ // Responsive Helper plugin to assist with responsive design breakpoints in the previewWindow, with CSS output & overrides
2
+ // Variation of the Brackets Response plugin idea at https://github.com/kidwm/brackets-response
3
+ // by Matt Pass as an ICEcoder plugin
4
+
5
+ top . ICEcoder . doResponsive = function ( ) {
6
+
7
+ // Set our initial values
8
+ if ( "undefined" === typeof breakPoints ) {
9
+ // Array to contain breakpoint px values, this current breakpoint and the template for media queries
10
+ breakPoints = [ ] ;
11
+ thisBreakPoint = 0 ;
12
+ mediaQueryTemplate = "@media only screen and (max-width: [[[WIDTH]]]) {" ;
13
+ }
14
+
15
+ // Init the plugin if we have a target window
16
+ if ( top . ICEcoder . previewWindow . location ) {
17
+
18
+ // Init the DOM objects if we haven't yet got a pw var
19
+ if ( "undefined" === typeof pw ) {
20
+
21
+ // Define the preview window target from the ICEcoder window perspective
22
+ pw = top . ICEcoder . previewWindow . document ;
23
+
24
+ // Define the 12 colors to be used in order for the breakpoints
25
+ colors = [ "#e1c76e" , /* yellow */
26
+ "#6cb5d9" , /* blue */
27
+ "#bf255c" , /* pink */
28
+ "#f9602c" , /* orange */
29
+ "#b9ca4a" , /* green */
30
+ "#9179bb" , /* purple */
31
+ "#d00" , /* red */
32
+ "#214e7b" , /* bright blue */
33
+ "#cc7" , /* grey-yellow */
34
+ "#099" , /* teal */
35
+ "#6a0d6a" , /* purple-pink */
36
+ "#186718" ] ; /* dark green */
37
+
38
+ // Define a bar at the top of the page to contain breakpoints and add button etc
39
+ respBar = document . createElement ( 'div' ) ;
40
+ respBar . style . position = "fixed" ;
41
+ respBar . style . top = "0" ;
42
+ respBar . style . left = "0" ;
43
+ respBar . style . width = "100%" ;
44
+ respBar . style . height = "20px" ;
45
+ respBar . style . background = "#141612" ;
46
+ respBar . style . boxSizing = "border-box" ;
47
+ respBar . style . zIndex = "1000001" ;
48
+ respBar . id = "ICEcoderRespBar" ;
49
+
50
+ // Now define an inner bar inside this, as wide as the body
51
+ respInnerBar = document . createElement ( 'div' ) ;
52
+ respInnerBar . style . position = "fixed" ;
53
+ respInnerBar . style . top = "0" ;
54
+ respInnerBar . style . width = pw . body . getBoundingClientRect ( ) . width + "px" ;
55
+ respInnerBar . style . height = "20px" ;
56
+ respInnerBar . style . background = "#666" ;
57
+ respInnerBar . style . boxSizing = "border-box" ;
58
+ respInnerBar . style . zIndex = "1000002" ;
59
+ respInnerBar . id = "ICEcoderRespInnerBar" ;
60
+
61
+ // Define a dummy element that will contain our cloned nodes
62
+ respInnerBarDummy = document . createElement ( 'div' ) ;
63
+ respInnerBarDummy . style . position = "absolute" ;
64
+ respInnerBarDummy . style . width = "1px" ;
65
+ respInnerBarDummy . style . left = "-10000px" ;
66
+ respInnerBarDummy . id = "respInnerBarDummy" ;
67
+
68
+ // OK now define our add button to allow the adding of breakpoints
69
+ add = document . createElement ( 'div' ) ;
70
+ add . style . position = "fixed" ;
71
+ add . style . display = "inline-block" ;
72
+ add . style . top = "0" ;
73
+ add . style . padding = "3px 8px" ;
74
+ add . style . fontFamily = "arial, verdana, helvetica, sans-serif" ;
75
+ add . style . fontSize = "12px" ;
76
+ add . style . background = "#2187e7" ;
77
+ add . style . color = "#eee" ;
78
+ add . style . cursor = "pointer" ;
79
+ add . style . zIndex = "1000003" ;
80
+ add . id = "ICEcoderRespAdd" ;
81
+ add . innerHTML = "Add + " ;
82
+ add . addEventListener ( "click" , function ( ) { addBreakpoint ( ) ; } , false ) ;
83
+
84
+ // Now a container for our 'focus box'
85
+ focusContBox = document . createElement ( 'div' ) ;
86
+ focusContBox . style . position = "absolute" ;
87
+ focusContBox . style . top = "0" ;
88
+ focusContBox . style . left = "0" ;
89
+ focusContBox . style . zIndex = "1000000" ;
90
+ focusContBox . id = "focusContBox" ;
91
+
92
+ // Plus the focus box within this, contains a massive outline to provide the focus 'scope'
93
+ focusBox = document . createElement ( 'div' ) ;
94
+ focusBox . style . position = "relative" ;
95
+ focusBox . style . display = "inline-block" ;
96
+ focusBox . style . top = "0" ;
97
+ focusBox . style . height = "0" ;
98
+ focusBox . style . width = "0" ;
99
+ focusBox . style . outline = "rgba(0,0,0,0.5) solid 10000px" ;
100
+ focusBox . style . transition = "all 0.1s ease-in-out" ;
101
+ focusBox . style . cursor = "pointer" ;
102
+ focusBox . style . zIndex = "1000000" ;
103
+ focusBox . id = "focusBox" ;
104
+ // Set an event listener to handle what happens when we click on it
105
+ focusBox . addEventListener ( "click" , function ( ) {
106
+
107
+ // If we've not got any breakpoints as yet, add one
108
+ if ( breakPoints . length === 0 ) {
109
+ addBreakpoint ( ) ;
110
+ }
111
+
112
+ // Select the last, or open, a /[NEW] tab for this content as needed
113
+ var openFiles = top . ICEcoder . openFiles ;
114
+ var foundNewFile = false ;
115
+ for ( var i = openFiles . length - 1 ; i >= 0 ; i -- ) {
116
+ if ( openFiles [ i ] == "/[NEW]" ) {
117
+ top . ICEcoder . switchTab ( i + 1 ) ;
118
+ foundNewFile = true ;
119
+ break ;
120
+ }
121
+ }
122
+ if ( ! foundNewFile ) {
123
+ top . ICEcoder . newTab ( ) ;
124
+ }
125
+
126
+ // Get CM instance for this tab
127
+ top . ICEcoder . targetcM = top . ICEcoder . getcMInstance ( ) ;
128
+
129
+ // We now need to find our media query chunk to add content into,
130
+ // start by getting all content lines into an array
131
+ var thisContent = top . ICEcoder . targetcM . getValue ( ) ;
132
+ thisContent = thisContent . split ( "\n" ) ;
133
+
134
+ // Clear any selection
135
+ top . ICEcoder . targetcM . setSelection ( { line :0 , ch :0 } , { line :0 , ch :0 } ) ;
136
+
137
+ // Establish the media query line to use
138
+ var mediaQueryLine = mediaQueryTemplate . replace ( "[[[WIDTH]]]" , breakPoints [ thisBreakPoint ] + "px" ) ;
139
+
140
+ // Set init values meaning the start and end of chunk not yet established
141
+ var chunkStartLine = - 1 ;
142
+ var chunkEndLine = top . ICEcoder . targetcM . lineCount ( ) - 1 ;
143
+ var selectorStartLine = - 1 ;
144
+ var selectorEndLine = - 1 ;
145
+ var foundSelector = false ;
146
+
147
+ // Try and find the start and end of the chunk we need to work within
148
+ for ( var i = 0 ; i < thisContent . length ; i ++ ) {
149
+
150
+ // Found the existing chunk!
151
+ if ( thisContent [ i ] == mediaQueryLine ) {
152
+ chunkStartLine = i ;
153
+ }
154
+ // If we found our start, we continue to the next media query or end of doc, from 1 line after the start
155
+ if ( chunkStartLine > - 1 && i > chunkStartLine ) {
156
+ if ( thisContent [ i ] . indexOf ( "@media" ) != - 1 ) {
157
+ chunkEndLine = i - 2 ;
158
+ }
159
+ // Have we found our selector within this media query?
160
+ if ( thisContent [ i ] . indexOf ( "\t" + getCSSPath ( top . ICEcoder . respElemSelected ) + " {" ) != - 1 ) {
161
+ foundSelector = true ;
162
+ selectorStartLine = i ;
163
+ }
164
+ // Finally, if we've found the line containing the end brace for the selector
165
+ if ( foundSelector && thisContent [ i ] == "\t}" ) {
166
+ selectorEndLine = i + 1 ;
167
+ break ;
168
+ }
169
+ }
170
+ }
171
+
172
+ // Set the media query line at the start of the file, with 2 line breaks if it's not the first one added
173
+ cssOutputMQ = top . ICEcoder . targetcM . getValue ( ) == "" ?mediaQueryLine + "\n" :"\n\n" + mediaQueryLine + "\n" ;
174
+
175
+ // Set the CSS rules within the selector (CSS path)
176
+ // The first line here establishes the CSS selector path by classname and node depth
177
+ cssOutputStart = "\t" + getCSSPath ( top . ICEcoder . respElemSelected ) + " {\n" ;
178
+ cssOutputRules = "" ;
179
+ for ( var key in diff ) {
180
+ cssOutputRules += "\t\t" + key + ": " + diff [ key ] + ";\n" ;
181
+ }
182
+ cssOutputEnd = "\t}\n" ;
183
+ // End of our media query
184
+ cssOutputMQEnd = "}" ;
185
+
186
+ // If we have a media query chunk
187
+ if ( chunkStartLine !== - 1 ) {
188
+ // If we don't have a selector in the media query chunk yet, add one in at the end of it
189
+ if ( ! foundSelector ) {
190
+ top . ICEcoder . targetcM . replaceRange ( cssOutputStart + cssOutputRules + cssOutputEnd , { line :chunkEndLine , ch :0 } , { line :chunkEndLine , ch :0 } ) ;
191
+ // We have our selector, so just select it
192
+ } else {
193
+ top . ICEcoder . targetcM . setSelection ( { line :selectorStartLine , ch :0 } , { line :selectorEndLine , ch :0 } ) ;
194
+
195
+ }
196
+ // Else insert media query chunk for first time (and our selector chunk)
197
+ } else {
198
+ top . ICEcoder . targetcM . setValue ( top . ICEcoder . targetcM . getValue ( ) + cssOutputMQ + cssOutputStart + cssOutputRules + cssOutputEnd + cssOutputMQEnd ) ;
199
+ }
200
+ } , false ) ;
201
+
202
+ // Finally, define our CSS output DIV to show DOM elem styles
203
+ outputBox = document . createElement ( 'div' ) ;
204
+ outputBox . style . position = "fixed" ;
205
+ outputBox . style . display = "block" ;
206
+ outputBox . style . bottom = "0" ;
207
+ outputBox . style . left = "0" ;
208
+ outputBox . style . width = "100%" ;
209
+ outputBox . style . height = "150px" ;
210
+ outputBox . style . fontFamily = "arial, verdana, helvetica, sans-serif" ;
211
+ outputBox . style . fontSize = "12px" ;
212
+ outputBox . style . background = "rgba(255,255,255,0.5)" ;
213
+ outputBox . style . color = "#000" ;
214
+ outputBox . style . overflow = "auto" ;
215
+ outputBox . style . zIndex = "1000000" ;
216
+ outputBox . id = "outputBox"
217
+ }
218
+
219
+ // Add DOM elems to previewWindow
220
+ pw . body . appendChild ( respBar ) ;
221
+ pw . body . appendChild ( respInnerBar ) ;
222
+ pw . body . appendChild ( respInnerBarDummy ) ;
223
+ pw . body . appendChild ( add ) ;
224
+ pw . body . appendChild ( focusContBox ) ;
225
+ pw . getElementById ( 'focusContBox' ) . appendChild ( focusBox ) ;
226
+ pw . body . appendChild ( outputBox ) ;
227
+
228
+ // Get all DOM elems into an array
229
+ elems = pw . body . getElementsByTagName ( '*' ) ;
230
+
231
+ // For each one of those, if it's got a z-index under 1000000, it should be a user set DOM elem
232
+ for ( var i = 0 ; i <= elems . length ; i ++ ) {
233
+ if ( elems [ i ] . style . zIndex < 1000000 ) {
234
+ // So add a mouseover event to it...
235
+ elems [ i ] . addEventListener ( "mouseover" , function ( ) {
236
+ // ...that sets the left, width and high properties of the focus box to match the bounding rectangle of the DOM elem
237
+ // ...top is a cumulative offset from the document top so we can scroll and it matches y pos of elem underneath
238
+ pw . getElementById ( 'focusBox' ) . style . top = cumulativeOffset ( this ) . top + "px" ;
239
+ pw . getElementById ( 'focusBox' ) . style . left = this . getBoundingClientRect ( ) . left + "px" ;
240
+ pw . getElementById ( 'focusBox' ) . style . width = this . getBoundingClientRect ( ) . width + "px" ;
241
+ pw . getElementById ( 'focusBox' ) . style . height = this . getBoundingClientRect ( ) . height + "px" ;
242
+
243
+ // Set the width (to same value), which forces a DOM render and avoids us getting lots of extra unwanted styles picked up
244
+ this . style . width = getComputedStyle ( this ) . width ;
245
+
246
+ // Create a dummy clone of the node...
247
+ var dummy = this . cloneNode ( true ) ;
248
+ // ...and insert that into our dummy container that's out of sight
249
+ pw . getElementById ( 'respInnerBarDummy' ) . appendChild ( dummy ) ;
250
+
251
+ // Now to pick up the styles. First get inline styles set on the elem
252
+ styleText = this . style . cssText . split ( ";" ) ;
253
+ // Get rid of last empty array item if it's empty
254
+ if ( styleText [ styleText . length - 1 ] == "" ) {
255
+ styleText . pop ( ) ;
256
+ }
257
+
258
+ // Get styles from the elem and dummy clone of it
259
+ var defaultStyles = getComputedStyle ( dummy ) ;
260
+ var elementStyles = getComputedStyle ( this ) ;
261
+
262
+ // Work out diffs between elem and dummy clone and but into a diff object
263
+ diff = { } ;
264
+ for ( var key in elementStyles ) {
265
+ // If the value of matching keys isn't the same, we have a value set as it's different
266
+ if ( defaultStyles [ key ] !== elementStyles [ key ] ) {
267
+ // We'll ignore cssText as we've got inline styles already
268
+ if ( key !== "cssText" ) {
269
+ // Set the CSS syntax key by turning camel case into hyphenated lowercase
270
+ diff [ key . replace ( / ( [ a - z \d ] ) ( [ A - Z ] ) / g, '$1-$2' ) . toLowerCase ( ) ] = elementStyles [ key ] ;
271
+ }
272
+ }
273
+ }
274
+
275
+ // Finally, create array of key/value style pairs, also trimming whitespace as we go
276
+ for ( var i = 0 ; i < styleText . length ; i ++ ) {
277
+ var thisItem = styleText [ i ] . split ( ":" ) ;
278
+ diff [ thisItem [ 0 ] . trim ( ) ] = thisItem [ 1 ] . trim ( ) ;
279
+ }
280
+
281
+ // Leave behind a pointer for this DOM elem
282
+ top . ICEcoder . respElemSelected = this ;
283
+
284
+ // Also output to the output box in a nice-ish format
285
+ pw . getElementById ( 'outputBox' ) . innerHTML = JSON . stringify ( diff ) . replace ( / \" \, / g, '"<br>' ) . replace ( / \{ / g, '' ) . replace ( / \} / g, '' ) ;
286
+
287
+ } , true ) ;
288
+ }
289
+ }
290
+ }
291
+ }
292
+
293
+ // Function to add breakpoints to the bar
294
+ function addBreakpoint ( ) {
295
+ // Work out the window width and height plus the body width from our bar
296
+ var winW = top . ICEcoder . previewWindow . outerWidth ;
297
+ var winH = top . ICEcoder . previewWindow . outerHeight ;
298
+ var bodyW = parseInt ( pw . getElementById ( 'ICEcoderRespInnerBar' ) . style . width , 10 ) ;
299
+ // Set the width to be the lesser of these
300
+ var newW = winW < bodyW ?winW :bodyW ;
301
+
302
+ // If we haven't got this width in our array yet, we can add it in
303
+ if ( breakPoints . indexOf ( newW ) == - 1 ) {
304
+ breakPoints . push ( newW ) ;
305
+ thisBreakPoint = breakPoints . length - 1 ;
306
+ var thisBP = thisBreakPoint ;
307
+
308
+ // Now setup a bar for this breakpoint, width as per our newW and with the next background color
309
+ thisRespBar = document . createElement ( 'div' ) ;
310
+ thisRespBar . style . position = "absolute" ;
311
+ thisRespBar . style . display = "inline-block" ;
312
+ thisRespBar . style . width = newW + "px" ;
313
+ thisRespBar . style . height = "20px" ;
314
+ thisRespBar . style . top = "0" ;
315
+ thisRespBar . style . boxSizing = "border-box" ;
316
+ thisRespBar . style . padding = "3px 8px" ;
317
+ thisRespBar . style . background = colors [ thisBreakPoint ] ;
318
+ thisRespBar . style . color = "#fff" ;
319
+ thisRespBar . style . fontFamily = "arial, verdana, helvetica, sans-serif" ;
320
+ thisRespBar . style . fontSize = "12px" ;
321
+ thisRespBar . style . textAlign = "right" ;
322
+ thisRespBar . style . cursor = "pointer" ;
323
+ thisRespBar . innerHTML = newW + "px" ;
324
+ thisRespBar . id = "respBar" + thisBreakPoint ;
325
+ // Set an event so on click it sets the breakpoint and changes window width to match
326
+ thisRespBar . addEventListener ( "click" , function ( ) {
327
+ setBreakPoint ( thisBP ) ;
328
+ changeWinW ( newW , winH ) ;
329
+ } , false ) ;
330
+ // Append that to our bar container
331
+ pw . getElementById ( 'ICEcoderRespInnerBar' ) . appendChild ( thisRespBar ) ;
332
+ // It exists already so trigger a click event to set it, set window size etc
333
+ } else {
334
+ pw . getElementById ( 'respBar' + breakPoints . indexOf ( newW ) ) . click ( ) ;
335
+ }
336
+ } ;
337
+
338
+ // Get a CSS path for given elem
339
+ function getCSSPath ( elem ) {
340
+ var cssPath = [ ] , item , entry ;
341
+
342
+ // Get all parent items
343
+ for ( item = elem . parentNode ; item ; item = item . parentNode ) {
344
+ entry = item . tagName . toLowerCase ( ) ;
345
+ if ( entry === "html" ) {
346
+ break ;
347
+ }
348
+ if ( item . className ) {
349
+ entry += "." + item . className . replace ( / / g, '.' ) ;
350
+ }
351
+ cssPath . push ( entry ) ;
352
+ }
353
+
354
+ // Reverse the array to run forwards
355
+ cssPath . reverse ( ) ;
356
+
357
+ // Add on this elem to the end of the array
358
+ entry = elem . tagName . toLowerCase ( ) ;
359
+ if ( elem . className ) {
360
+ entry += "." + elem . className . replace ( / / g, '.' ) ;
361
+ }
362
+ cssPath . push ( entry ) ;
363
+
364
+ // Return the array as a space delimited string
365
+ return cssPath . join ( " " ) ;
366
+ } ;
367
+
368
+ // Change the window size on demand
369
+ function changeWinW ( winW , winH ) {
370
+ top . ICEcoder . previewWindow . resizeTo ( winW , winH ) ;
371
+ } ;
372
+
373
+ // Set the current breakpoint to one we've clicked on
374
+ function setBreakPoint ( newBP ) {
375
+ thisBreakPoint = newBP ;
376
+ } ;
377
+
378
+ // Work out the offset from the top of the page
379
+ var cumulativeOffset = function ( element ) {
380
+ var top = 0 , left = 0 ;
381
+ do {
382
+ top += element . offsetTop || 0 ;
383
+ left += element . offsetLeft || 0 ;
384
+ element = element . offsetParent ;
385
+ } while ( element ) ;
386
+
387
+ // Return it as an array
388
+ return {
389
+ top :top ,
390
+ left :left
391
+ } ;
392
+ } ;