1 /* See license.txt for terms of usage */
  2 
  3 define([
  4     "firebug/lib/object",
  5     "firebug/firebug",
  6     "firebug/lib/domplate",
  7     "firebug/lib/locale",
  8     "firebug/lib/events",
  9     "firebug/lib/css",
 10     "firebug/lib/dom",
 11     "firebug/lib/xml",
 12     "firebug/chrome/menu",
 13     "firebug/editor/editor",
 14 ],
 15 function(Obj, Firebug, Domplate, Locale, Events, Css, Dom, Xml, Menu) {
 16 
 17 // ************************************************************************************************
 18 
 19 function LayoutPanel() {}
 20 
 21 with (Domplate) {
 22 LayoutPanel.prototype = Obj.extend(Firebug.Panel,
 23 {
 24     template: domplate(
 25     {
 26         tag:
 27             DIV({"class": "outerLayoutBox"},
 28                 DIV({"class": "positionLayoutBox $outerTopMode $outerRightMode $outerBottomMode "+
 29                         "$outerLeftMode focusGroup"},
 30                     DIV({"class": "layoutEdgeTop layoutEdge"}),
 31                     DIV({"class": "layoutEdgeRight layoutEdge"}),
 32                     DIV({"class": "layoutEdgeBottom layoutEdge"}),
 33                     DIV({"class": "layoutEdgeLeft layoutEdge"}),
 34 
 35                     DIV({"class": "layoutLabelBottom layoutLabel layoutLabelPosition"},
 36                         SPAN({"class": "layoutPosition layoutCaption",
 37                                 "aria-label": Locale.$STR("a11y.layout.position")},
 38                             Locale.$STR("position") + ": " + "$position"),
 39                         SPAN({"class": "layoutBoxSizing layoutCaption",
 40                                 "aria-label": Locale.$STR("a11y.layout.box-sizing")},
 41                             Locale.$STR("a11y.layout.box-sizing") + ": " + "$boxSizing"),
 42                         SPAN({"class": "layoutZIndex", $invisible: "$zIndex|isInvisible",
 43                                 "aria-label": Locale.$STR("a11y.layout.z-index")},
 44                             "z: " + "$zIndex")
 45                     ),
 46 
 47                     DIV({"class": "layoutLabelTop layoutLabel",
 48                             $invisible: "$outerTop|isInvisible"},
 49                         SPAN({"class": "layoutLabelOuterTop editable focusStart",
 50                                 "aria-label": Locale.$STR("a11y.layout.position top")},
 51                             "$outerTop"
 52                         )
 53                     ),
 54                     DIV({"class": "layoutLabelRight layoutLabel",
 55                             $invisible: "$outerRight|isInvisible"},
 56                         SPAN({"class": "layoutLabelOuterRight editable",
 57                                 "aria-label": Locale.$STR("a11y.layout.position right")},
 58                             "$outerRight"
 59                         )
 60                     ),
 61                     DIV({"class": "layoutLabelBottom layoutLabel",
 62                             $invisible: "$outerBottom|isInvisible"},
 63                         SPAN({"class": "layoutLabelOuterBottom editable",
 64                                 "aria-label": Locale.$STR("a11y.layout.position bottom")},
 65                             "$outerBottom"
 66                         )
 67                     ),
 68                     DIV({"class": "layoutLabelLeft layoutLabel",
 69                             $invisible: "$outerLeft|isInvisible"},
 70                         SPAN({"class": "layoutLabelOuterLeft editable",
 71                                 "aria-label": Locale.$STR("a11y.layout.position left")},
 72                             "$outerLeft"
 73                         )
 74                     ),
 75 
 76                     DIV({"class": "outerLabel layoutCaption"}, "$outerLabel"),
 77 
 78 
 79                     DIV({"class": "marginLayoutBox layoutBox editGroup focusGroup"},
 80                         DIV({"class": "layoutCaption"}, Locale.$STR("LayoutMargin")),
 81                         DIV({"class": "layoutLabelTop layoutLabel",
 82                                 $invisible: "$marginTop|isInvisible"},
 83                             SPAN({"class": "layoutLabelMarginTop editable focusStart",
 84                                     "aria-label": Locale.$STR("a11y.layout.margin top")},
 85                                 "$marginTop"
 86                             )
 87                         ),
 88                         DIV({"class": "layoutLabelRight layoutLabel",
 89                                 $invisible: "$marginRight|isInvisible"},
 90                             SPAN({"class": "layoutLabelMarginRight editable",
 91                                     "aria-label": Locale.$STR("a11y.layout.margin right")},
 92                                 "$marginRight"
 93                             )
 94                         ),
 95                         DIV({"class": "layoutLabelBottom layoutLabel",
 96                                 $invisible: "$marginBottom|isInvisible"},
 97                             SPAN({"class": "layoutLabelMarginBottom editable",
 98                                     "aria-label": Locale.$STR("a11y.layout.margin bottom")},
 99                                 "$marginBottom"
100                             )
101                         ),
102                         DIV({"class": "layoutLabelLeft layoutLabel",
103                                 $invisible: "$marginLeft|isInvisible"},
104                             SPAN({"class": "layoutLabelMarginLeft editable",
105                                     "aria-label": Locale.$STR("a11y.layout.margin left")},
106                                 "$marginLeft"
107                             )
108                         ),
109 
110                         DIV({"class": "borderLayoutBox layoutBox editGroup focusGroup"},
111                             DIV({"class": "layoutCaption"}, Locale.$STR("LayoutBorder")),
112                             DIV({"class": "layoutLabelTop layoutLabel",
113                                     $invisible: "$borderTop|isInvisible"},
114                                 SPAN({"class": "layoutLabelBorderTop editable  focusStart",
115                                         "aria-label": Locale.$STR("a11y.layout.border top")},
116                                     "$borderTop"
117                                 )
118                             ),
119                             DIV({"class": "layoutLabelRight layoutLabel",
120                                     $invisible: "$borderRight|isInvisible"},
121                                 SPAN({"class": "layoutLabelBorderRight editable",
122                                         "aria-label": Locale.$STR("a11y.layout.border right")},
123                                     "$borderRight"
124                                 )
125                             ),
126                             DIV({"class": "layoutLabelBottom layoutLabel",
127                                     $invisible: "$borderBottom|isInvisible"},
128                                 SPAN({"class": "layoutLabelBorderBottom editable",
129                                         "aria-label": Locale.$STR("a11y.layout.border bottom")},
130                                     "$borderBottom"
131                                 )
132                             ),
133                             DIV({"class": "layoutLabelLeft layoutLabel",
134                                     $invisible: "$borderLeft|isInvisible"},
135                                 SPAN({"class": "layoutLabelBorderLeft editable",
136                                         "aria-label": Locale.$STR("a11y.layout.border left")},
137                                     "$borderLeft"
138                                 )
139                             ),
140 
141                             DIV({"class": "paddingLayoutBox layoutBox editGroup focusGroup"},
142                                 DIV({"class": "layoutCaption"}, Locale.$STR("LayoutPadding")),
143                                 DIV({"class": "layoutLabelTop layoutLabel",
144                                         $invisible: "$paddingTop|isInvisible"},
145                                     SPAN({"class": "layoutLabelPaddingTop editable focusStart",
146                                             "aria-label": Locale.$STR("a11y.layout.padding top")},
147                                         "$paddingTop"
148                                     )
149                                 ),
150                                 DIV({"class": "layoutLabelRight layoutLabel",
151                                         $invisible: "$paddingRight|isInvisible"},
152                                     SPAN(
153                                         {
154                                             "class": "layoutLabelPaddingRight editable",
155                                             "aria-label":
156                                                 Locale.$STR("a11y.layout.padding right")
157                                         },
158                                         "$paddingRight"
159                                     )
160                                 ),
161                                 DIV({"class": "layoutLabelBottom layoutLabel",
162                                         $invisible: "$paddingBottom|isInvisible"},
163                                     SPAN(
164                                         {
165                                             "class": "layoutLabelPaddingBottom editable",
166                                             "aria-label":
167                                                 Locale.$STR("a11y.layout.padding bottom")
168                                         },
169                                         "$paddingBottom"
170                                     )
171                                 ),
172                                 DIV({"class": "layoutLabelLeft layoutLabel",
173                                         $invisible: "$paddingLeft|isInvisible"},
174                                     SPAN({"class": "layoutLabelPaddingLeft editable",
175                                             "aria-label": Locale.$STR("a11y.layout.padding left")},
176                                         "$paddingLeft"
177                                     )
178                                 ),
179 
180                                 DIV({"class": "contentLayoutBox layoutBox editGroup focusGroup"},
181                                     DIV({"class": "layoutLabelCenter layoutLabel"},
182                                         SPAN({"class": "layoutLabelWidth layoutLabel editable "+
183                                                 "focusStart",
184                                                 "aria-label": Locale.$STR("a11y.layout.width")},
185                                             "$width"
186                                         ),
187                                         " x ",
188                                         SPAN({"class": "layoutLabelHeight layoutLabel editable",
189                                                 "aria-label": Locale.$STR("a11y.layout.height")},
190                                             "$height"
191                                         )
192                                     )
193                                 )
194                             )
195                         )
196                     )
197                 )
198             ),
199 
200         isInvisible: function(value)
201         {
202             return value == 0;
203         },
204 
205         getVerticalText: function(n)
206         {
207             return getVerticalText(n);
208         }
209     }),
210 
211     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
212 
213     onMouseOver: function(event)
214     {
215         var layoutBox = Dom.getAncestorByClass(event.target, "layoutBox");
216         var boxFrame = layoutBox ? getBoxFrame(layoutBox) : null;
217 
218         if (this.highlightedBox)
219             Css.removeClass(this.highlightedBox, "highlighted");
220 
221         this.highlightedBox = layoutBox;
222 
223         if (layoutBox)
224             Css.setClass(layoutBox, "highlighted");
225 
226         Firebug.Inspector.highlightObject(this.selection, this.context, "boxModel", boxFrame);
227     },
228 
229     onMouseOut: function(event)
230     {
231         var nextTarget = event.relatedTarget;
232         if (nextTarget && Dom.getAncestorByClass(nextTarget, "layoutBox"))
233             return;
234 
235         if (this.highlightedBox)
236             Css.removeClass(this.highlightedBox, "highlighted");
237 
238         this.highlightedBox = null;
239 
240         Firebug.Inspector.highlightObject(null, null, "boxModel");
241     },
242 
243     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
244     // extends Panel
245 
246     name: "layout",
247     parentPanel: "html",
248     order: 2,
249     enableA11y: true,
250 
251     initialize: function()
252     {
253         this.onMouseOver = Obj.bind(this.onMouseOver, this);
254         this.onMouseOut = Obj.bind(this.onMouseOut, this);
255         this.onAfterPaint = Obj.bindFixed(this.refresh, this);
256 
257         Firebug.Panel.initialize.apply(this, arguments);
258     },
259 
260     initializeNode: function(oldPanelNode)
261     {
262         Events.addEventListener(this.panelNode, "mouseover", this.onMouseOver, false);
263         Events.addEventListener(this.panelNode, "mouseout", this.onMouseOut, false);
264 
265         Firebug.Panel.initializeNode.apply(this, arguments);
266     },
267 
268     destroyNode: function()
269     {
270         Events.removeEventListener(this.panelNode, "mouseover", this.onMouseOver, false);
271         Events.removeEventListener(this.panelNode, "mouseout", this.onMouseOut, false);
272 
273         Firebug.Panel.destroyNode.apply(this, arguments);
274     },
275 
276     show: function(state)
277     {
278         Events.addEventListener(this.context.browser, "MozAfterPaint", this.onAfterPaint, true);
279     },
280 
281     hide: function()
282     {
283         Events.removeEventListener(this.context.browser, "MozAfterPaint", this.onAfterPaint, true);
284     },
285     
286     supportsObject: function(object, type)
287     {
288         return object instanceof window.Element ? 1 : 0;
289     },
290 
291     refresh: function()
292     {
293         this.updateSelection(this.selection);
294     },
295 
296     updateSelection: function(element)
297     {
298         var view = element ? element.ownerDocument.defaultView : null;
299         if (!view)
300             return this.panelNode.textContent = "";
301 
302         var prev = Dom.getPreviousElement(element.previousSibling);
303         var next = Dom.getNextElement(element.nextSibling);
304 
305         var style = view.getComputedStyle(element, "");
306 
307         var args = Css.getBoxFromStyles(style, element);
308 
309         args.outerLeft = args.outerRight = args.outerTop = args.outerBottom = '';
310         args.outerLeftMode = args.outerRightMode = args.outerTopMode = args.outerBottomMode = "";
311         args.zIndex = args.zIndex ? args.zIndex : "auto";
312 
313         var boxSizing = style.getPropertyCSSValue("box-sizing") ||
314             style.getPropertyCSSValue("-moz-box-sizing");
315         args.boxSizing = boxSizing.cssText;
316 
317         var position = style.getPropertyCSSValue("position").cssText;
318         args.position = position;
319         args.outerLabel = "";
320 
321         if (Xml.isElementSVG(element) || Xml.isElementMathML(element) || Xml.isElementXUL(element))
322         {
323             var rect = element.getBoundingClientRect();
324             // XXXjjb I believe this is incorrect. We should use the value as given by the call
325             //if (rect.wrappedJSObject)
326             //    rect = rect.wrappedJSObject;
327 
328             args.width = Math.round(rect.width);
329             args.height = Math.round(rect.height);
330         }
331 
332         // these Modes are classes on the domplate
333         args.outerLeftMode = args.outerRightMode = args.outerTopMode = args.outerBottomMode =
334             "blankEdge";
335 
336         if (position == "absolute" || position == "fixed" || position == "relative")
337         {
338             function getStyle(style, name)
339             {
340                 var value = style.getPropertyCSSValue(name);
341                 return value && value.cssText ? parseInt(value.cssText) : " ";
342             }
343 
344             args.outerLabel = Locale.$STR("LayoutPosition");
345 
346             args.outerLeft = getStyle(style, "left");
347             args.outerTop = getStyle(style, "top");
348             args.outerRight = getStyle(style, "right");
349             args.outerBottom = getStyle(style, "bottom");
350 
351             args.outerLeftMode = args.outerRightMode = args.outerTopMode = args.outerBottomMode =
352                 "absoluteEdge";
353         }
354 
355         var node;
356         // If the layout panel content was already created, just fill in the new values
357         if (this.panelNode.getElementsByClassName("outerLayoutBox").item(0))
358         {
359             // The styles for the positionLayoutBox need to be set manually
360             var positionLayoutBox = this.panelNode.getElementsByClassName("positionLayoutBox").
361                 item(0);
362             positionLayoutBox.className = "positionLayoutBox "+args.outerTopMode+" "+
363                 args.outerRightMode+" "+args.outerBottomMode+" "+args.outerLeftMode+" focusGroup";
364 
365             var values =
366             {
367                 layoutPosition: {label: Locale.$STR("position"), value: "position"},
368                 layoutBoxSizing: {label: Locale.$STR("a11y.layout.box-sizing"),
369                     value: "boxSizing"},
370                 layoutZIndex: {label: "z", value: "zIndex"},
371                 layoutLabelOuterTop: {value: "outerTop"},
372                 layoutLabelOuterRight: {value: "outerRight"},
373                 layoutLabelOuterBottom: {value: "outerBottom"},
374                 layoutLabelOuterLeft: {value: "outerLeft"},
375                 layoutLabelMarginTop: {value: "marginTop"},
376                 layoutLabelMarginRight: {value: "marginRight"},
377                 layoutLabelMarginBottom: {value: "marginBottom"},
378                 layoutLabelMarginLeft: {value: "marginLeft"},
379                 layoutLabelBorderTop: {value: "borderTop"},
380                 layoutLabelBorderRight: {value: "borderRight"},
381                 layoutLabelBorderBottom: {value: "borderBottom"},
382                 layoutLabelBorderLeft: {value: "borderLeft"},
383                 layoutLabelPaddingTop: {value: "paddingTop"},
384                 layoutLabelPaddingRight: {value: "paddingRight"},
385                 layoutLabelPaddingBottom: {value: "paddingBottom"},
386                 layoutLabelPaddingLeft: {value: "paddingLeft"},
387                 layoutLabelWidth: {value: "width"},
388                 layoutLabelHeight: {value: "height"},
389                 outerLabel: {value: "outerLabel"},
390             }
391 
392             for (val in values)
393             {
394                 var element = this.panelNode.getElementsByClassName(val).item(0);
395 
396                 element.textContent = values[val].label ?
397                     values[val].label+": "+args[values[val].value] : args[values[val].value];
398 
399                 if (this.template.isInvisible(args[values[val].value]))
400                     Css.setClass(element.parentNode, "invisible");
401                 else
402                     Css.removeClass(element.parentNode, "invisible");
403             }
404         }
405         else
406         {
407             node = this.template.tag.replace(args, this.panelNode);
408         }
409 
410         this.adjustCharWidth(this.getMaxCharWidth(args, node), this.panelNode);
411 
412         Events.dispatch(this.fbListeners, "onLayoutBoxCreated", [this, node, args]);
413     },
414 
415     /*
416      * The nested boxes of the Layout panel have digits which need to fit between the boxes.
417      * @param maxWidth: pixels the largest digit string
418      * @param node: panelNode to be adjusted (from tag:)
419      */
420     adjustCharWidth: function(maxWidth, node)
421     {
422         maxWidth += 10; // margin
423         if (maxWidth < 20)
424             maxWidth = 20;
425 
426         this.adjustBoxWidth(node, "marginLayoutBox", maxWidth);
427         this.adjustBoxWidth(node, "borderLayoutBox", maxWidth);
428         this.adjustBoxWidth(node, "paddingLayoutBox", maxWidth);
429 
430         var box = node.getElementsByClassName("outerLayoutBox").item(0);
431         box.style.cssText = "width: "+(240 + 3*maxWidth)+"px;";  // defaults to 300px
432 
433         this.adjustLabelWidth(node, "layoutLabelLeft", maxWidth);
434         this.adjustLabelWidth(node, "layoutLabelRight", maxWidth);
435     },
436 
437     /*
438      * By adjusting this width, the labels can be centered.
439      */
440     adjustLabelWidth: function(node, labelName, maxWidth)
441     {
442         var labels = node.getElementsByClassName(labelName);
443         for (var i = 0; i < labels.length; i++)
444             labels[i].style.cssText = "width: "+maxWidth+"px;";
445     },
446 
447     adjustBoxWidth: function(node, boxName, width)
448     {
449         var box = node.getElementsByClassName(boxName).item(0);
450         box.style.cssText = "right: "+width + 'px;'+" left: "+width + "px;";
451     },
452 
453     getMaxCharWidth: function(args, node)
454     {
455         Firebug.MeasureBox.startMeasuring(node);
456         var maxWidth = Math.max(
457                 Firebug.MeasureBox.measureText(args.marginLeft+"").width,
458                 Firebug.MeasureBox.measureText(args.marginRight+"").width,
459                 Firebug.MeasureBox.measureText(args.borderLeft+"").width,
460                 Firebug.MeasureBox.measureText(args.borderRight+"").width,
461                 Firebug.MeasureBox.measureText(args.paddingLeft+"").width,
462                 Firebug.MeasureBox.measureText(args.paddingRight+"").width
463                 );
464         Firebug.MeasureBox.stopMeasuring();
465         return maxWidth;
466     },
467 
468     getOptionsMenuItems: function()
469     {
470         return [
471             Menu.optionMenu("ShowRulers", "showRulers",
472                 "layout.option.tip.Show_Rulers")
473         ];
474     },
475 
476     getEditor: function(target, value)
477     {
478         if (!this.editor)
479             this.editor = new LayoutEditor(this.document);
480 
481         return this.editor;
482     }
483 });
484 
485 // ************************************************************************************************
486 // LayoutEditor
487 
488 function LayoutEditor(doc)
489 {
490     this.initializeInline(doc);
491 
492     this.noWrap = false;
493     this.numeric = true;
494 }
495 
496 LayoutEditor.prototype = domplate(Firebug.InlineEditor.prototype,
497 {
498     saveEdit: function(target, value, previousValue)
499     {
500         if (!this.panel.selection.style)
501             return;
502 
503         var labelBox = Dom.getAncestorByClass(target, "layoutLabel");
504         var layoutBox = getLayoutBox(labelBox);
505 
506         var boxFrame = getBoxFrame(layoutBox);
507         var boxEdge = getBoxEdge(labelBox);
508 
509         var styleName;
510         if (boxFrame == "content" || boxFrame == "position")
511             styleName = boxEdge.toLowerCase();
512         else if (boxFrame == "border")
513             styleName = boxFrame+boxEdge+"Width";
514         else
515             styleName = boxFrame+boxEdge;
516 
517         var intValue = value ? value : 0;
518         this.panel.selection.style[styleName] = intValue + "px";
519 
520         if (Firebug.Inspector.highlightedElement == this.panel.selection)
521         {
522             var boxFrame = this.highlightedBox ? getBoxFrame(this.highlightedBox) : null;
523             Firebug.Inspector.highlightObject(this.panel.selection, this.panel.context, "boxModel", boxFrame);
524         }
525 
526         if (Css.hasClass(target, "layoutVerticalText"))
527             target.innerHTML = getVerticalText(intValue);
528         else
529             target.textContent = intValue;
530     },
531 
532     endEditing: function(target, value, cancel)
533     {
534         // Don't remove groups
535         return false;
536     }
537 })};
538 
539 // ************************************************************************************************
540 // Local Helpers
541 
542 function getLayoutBox(element)
543 {
544     var re = /([^\s]+)LayoutBox/;
545     for (var box = element; box; box = box.parentNode)
546     {
547         if (re.exec(box.className))
548             return box;
549     }
550 }
551 
552 function getBoxFrame(element)
553 {
554     var re = /([^\s]+)LayoutBox/;
555     var m = re.exec(element.className);
556     return m ? m[1] : "";
557 }
558 
559 function getBoxEdge(element)
560 {
561     var re = /layoutLabel([^\s]+)/;
562     var m = re.exec(element.className);
563     return m ? m[1] : "";
564 }
565 
566 function getVerticalText(n)
567 {
568     n = n+"";
569     var text = [];
570     for (var i = 0; i < n.length; ++i)
571         text.push(n[i]);
572     return text.join("<br>");
573 }
574 
575 // ************************************************************************************************
576 // Registration
577 
578 Firebug.registerPanel(LayoutPanel);
579 
580 return LayoutPanel;  // move into Firebug.Layout ?
581 
582 // ************************************************************************************************
583 });
584