[Bps-public-commit] rt-extension-lifecycleui branch, master, updated. 6bf469d8ed93f0b4c3727df8e08b5dcad484ad63

Shawn Moore shawn at bestpractical.com
Wed Sep 6 17:12:42 EDT 2017


The branch, master has been updated
       via  6bf469d8ed93f0b4c3727df8e08b5dcad484ad63 (commit)
       via  b93f3b43573cfb1b7b7801a996b4aee19e322bb6 (commit)
       via  47bbc7c2c1cf772c95ea1fef4d6329014b1205ab (commit)
       via  37f0ab466519471d2cc72027d52140080fa7abcf (commit)
       via  0549d86bee25323ce5cf155051539a38bfce9a53 (commit)
       via  e6692748815d94f85e64f1eb6362b9b96701087f (commit)
       via  34df98b535e0fe9970921c3121bad0c3ea1bb7da (commit)
       via  7a8c20807bf1f323b76d7cb299b6ae213d4b6b48 (commit)
       via  8c58f9ae3782b1dfef935204180df1b910cdab66 (commit)
       via  2a3120e2162acbecfe4b8b2e44cff96cb08b4457 (commit)
       via  75ee81a2f4e2977eab17952aa4c9184512af5156 (commit)
       via  bdd22b1d265f1ac84361c4f89f1dc02b643b778e (commit)
       via  3b2731f54631bd7b97a2c83487df7170fa76a1ca (commit)
       via  d16c84cc6da514601c5d05fa6d31c2957f1ef712 (commit)
       via  f0b6d41190530a15d2dfc400235b69f3575942eb (commit)
       via  d09952366ea1d23bccf3a84aabb6fc18bc02ba04 (commit)
       via  47e12bc3a99578933f4aa85dd67dbb75db736630 (commit)
       via  1cf872140af58a072c3f7c369a4e1ae42dc3c1ac (commit)
       via  9cec6553a2c964fe07f3505d7d1e36422ca08418 (commit)
       via  750a3b93d451a7c0f4bc0f2caced160477050d30 (commit)
       via  41e908546758f51cbe1d9bd66d683012469f5f5d (commit)
       via  90af7e5022338fb287698977b55dc4c8a2e6fc9f (commit)
       via  7d57557fea2932cdd45ba89d1da849dddc922617 (commit)
       via  12381fa94f081fb958a2fd2c14a2d89d25595524 (commit)
       via  4cbb0fe92e67328d409f1be8aeadb4829eb73501 (commit)
       via  63df0d2a85d676aee5c511f72c86b37d4e358050 (commit)
       via  b49bb8d5e18e04c94d331eeb23b39e9bfa77d4ec (commit)
       via  fb1bf8b37b1f8ba954c32efd007dcfcb3f343595 (commit)
       via  62460e54f5c2e9a8eeef4010a89f745ac1c78f59 (commit)
       via  528f3d35a8ddbb4c1f52afec5814fc755a132380 (commit)
       via  07248beddf697290e0e36601b10741d96de70c37 (commit)
       via  4c659cdc6b5f1efec06fce4f37a3b9ca6b68d552 (commit)
       via  7bf688726ebd0440c2bfd2eb7e78aa1ffe0b2c94 (commit)
       via  e5b0a09c04bbbde8f1cd3e16f6783d2808a50030 (commit)
       via  37ed7e1b2c41608e28502514d64681edd40f7f13 (commit)
       via  a1d6e94c289f7115cfecd71ee25ae8590cd25bbf (commit)
       via  c1ce8d0beaa961d29d598c32fa3c6ede59b2f90e (commit)
       via  fe68211a86fcb33be89d143a2d25c0f62c839c0d (commit)
       via  7383f6a2e2d8449a6a2f56cf9b808f28ed23f1ab (commit)
       via  d956e9236e4e38c4ab022c5bbe632406a4fbb9f3 (commit)
       via  8d8396b3329ad413bc04000789a7d884ab089e5e (commit)
       via  3fff1d3a0fb058a7ee910f67f36b53a1188344a8 (commit)
       via  7088166bdb9e4f2be48a3279fa60a6c043fcad80 (commit)
       via  a963f89bef9f5f435033bfffa5ac37d9685b5f61 (commit)
       via  d083e81808e65470b3b72090a92e5359fb5c7958 (commit)
       via  a83dadf5f4fa573d7b56374c22fad88c40e72247 (commit)
       via  c240250dc39b826028914d4758ffbc5246e882f9 (commit)
       via  bb19541e5f8a5061b20295a70e7a1e4c47dee6cc (commit)
       via  e4f98086eb6deba898acf00df12ae588408678bf (commit)
      from  73fbec0b0b5c65e2986193b2712ed4146f7cf1be (commit)

Summary of changes:
 html/Admin/Lifecycles/index.html                   |  37 ++--
 .../Ticket/Elements/ShowSummary/AfterReminders     |   3 +-
 html/Elements/LifecycleGraph                       |  30 ++-
 html/Elements/LifecycleGraphExtras                 |  23 ++
 html/Elements/LifecycleInspector                   |   8 +
 html/Elements/LifecycleInspectorAction             |   2 +-
 html/Elements/LifecycleInspectorCanvas             |  95 +++++++-
 html/Elements/LifecycleInspectorCircle             |  16 ++
 html/Elements/LifecycleInspectorLine               |  28 +++
 html/Elements/LifecycleInspectorPolygon            |   8 +-
 html/Elements/LifecycleInspectorStatus             |   4 +-
 html/Elements/LifecycleInspectorText               |   4 +-
 html/Elements/LifecycleInspectorTransition         |   3 +-
 static/css/lifecycleui-editor.css                  | 102 +++++----
 static/css/lifecycleui-viewer.css                  |  64 +++++-
 static/js/lifecycleui-editor.js                    | 206 +++++++++++++-----
 static/js/lifecycleui-model.js                     | 160 +++++++++++++-
 static/js/lifecycleui-viewer.js                    | 242 ++++++++++++++++++---
 18 files changed, 841 insertions(+), 194 deletions(-)
 create mode 100644 html/Elements/LifecycleGraphExtras
 create mode 100644 html/Elements/LifecycleInspectorCircle
 create mode 100644 html/Elements/LifecycleInspectorLine

- Log -----------------------------------------------------------------
commit e4f98086eb6deba898acf00df12ae588408678bf
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 13:35:00 2017 +0000

    Avoid errors when loading config-file lifecycles

diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index f63797a..7514913 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -38,7 +38,13 @@ jQuery(function () {
             if (config[type]) {
                 self.statuses = self.statuses.concat(config[type]);
                 jQuery.each(config[type], function (j, statusName) {
-                    var item = config.statusExtra[statusName] || {};
+                    var item;
+                    if (config.statusExtra) {
+                        item = config.statusExtra[statusName] || {};
+                    }
+                    else {
+                        item = {};
+                    }
                     item._key  = _ELEMENT_KEY_SEQ++;
                     item._type = 'status';
                     item.name  = statusName;
@@ -78,7 +84,13 @@ jQuery(function () {
                 else {
                     jQuery.each(toList, function (i, toStatus) {
                         var description = fromStatus + ' -> ' + toStatus;
-                        var transition = config.transitionExtra[description] || {};
+                        var transition;
+                        if (config.transitionExtra) {
+                            transition = config.transitionExtra[description] || {};
+                        }
+                        else {
+                            transition = {};
+                        }
                         transition._key    = _ELEMENT_KEY_SEQ++;
                         transition._type   = 'transition';
                         transition.from    = fromStatus;
@@ -164,7 +176,7 @@ jQuery(function () {
         jQuery.each(['text', 'polygon'], function (i, type) {
             var decorations = [];
 
-            if (config.decorations[type]) {
+            if (config.decorations && config.decorations[type]) {
                 jQuery.each(config.decorations[type], function (i, decoration) {
                     decoration._key = _ELEMENT_KEY_SEQ++;
                     decoration._type = type;

commit bb19541e5f8a5061b20295a70e7a1e4c47dee6cc
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 13:46:58 2017 +0000

    Center lifecycle UI on ticket's current status

diff --git a/html/Elements/LifecycleGraph b/html/Elements/LifecycleGraph
index da6ddfd..fb36d64 100644
--- a/html/Elements/LifecycleGraph
+++ b/html/Elements/LifecycleGraph
@@ -12,12 +12,18 @@
             jQuery(".lifecycle-ui#lifecycle-<% $id %>").each(function () {
                 var container = this;
                 var config = <% JSON($config) |n %>;
+% if ($Ticket) {
+                var ticketStatus = <% $Ticket->Status | j%>;
+% } else {
+                var ticketStatus = undefined;
+% }
+
 % if ($Editing) {
                 var editor = new RT.LifecycleEditor();
-                editor.initializeEditor(container, config);
+                editor.initializeEditor(container, config, ticketStatus);
 % } else {
                 var viewer = new RT.LifecycleViewer();
-                viewer.initializeViewer(container, config);
+                viewer.initializeViewer(container, config, ticketStatus);
 % }
             });
         });
diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 55e8b9d..7a25693 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -391,7 +391,7 @@ jQuery(function () {
         this.selectDecoration(text._key);
     };
 
-    Editor.prototype.initializeEditor = function (node, config) {
+    Editor.prototype.initializeEditor = function (node, config, focusStatus) {
         var self = this;
         self.initializeViewer(node, config);
 
diff --git a/static/js/lifecycleui-viewer.js b/static/js/lifecycleui-viewer.js
index 3b582db..719a0d4 100644
--- a/static/js/lifecycleui-viewer.js
+++ b/static/js/lifecycleui-viewer.js
@@ -219,7 +219,23 @@ jQuery(function () {
         this.renderDecorations(initial);
     };
 
-    Viewer.prototype.initializeViewer = function (node, config) {
+    Viewer.prototype.centerOnItem = function (item) {
+        var x = this.xScale(item.x);
+        var y = this.yScale(item.y);
+        this.svg.selectAll("g")
+                .call(this._zoom.translateTo, x, y);
+    };
+
+    Viewer.prototype.focusOnStatus = function (statusName) {
+        if (!statusName) {
+            return;
+        }
+
+        var meta = this.lifecycle.statusObjectForName(statusName);
+        this.centerOnItem(meta);
+    };
+
+    Viewer.prototype.initializeViewer = function (node, config, focusStatus) {
         var self = this;
 
         self.container = jQuery(node);
@@ -241,6 +257,8 @@ jQuery(function () {
         self.createArrowHead();
         self.addZoomBehavior();
 
+        self.focusOnStatus(focusStatus);
+
         self.renderDisplay(true);
     };
 

commit c240250dc39b826028914d4758ffbc5246e882f9
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 14:23:41 2017 +0000

    Invalid transitions are always absent

diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index 7514913..3992d18 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -395,7 +395,7 @@ jQuery(function () {
     };
 
     Lifecycle.prototype.hasTransition = function (fromStatus, toStatus) {
-        if (fromStatus == toStatus) {
+        if (fromStatus == toStatus || !fromStatus || !toStatus) {
             return false;
         }
 

commit a83dadf5f4fa573d7b56374c22fad88c40e72247
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 14:31:57 2017 +0000

    Make arrowhead in markup not JS

diff --git a/html/Elements/LifecycleGraph b/html/Elements/LifecycleGraph
index fb36d64..9c62aef 100644
--- a/html/Elements/LifecycleGraph
+++ b/html/Elements/LifecycleGraph
@@ -1,5 +1,6 @@
 <div class="lifecycle-ui" id="lifecycle-<% $id %>">
     <svg<% $Editing ? ' class="editing"' : '' |n %>>
+        <& /Elements/LifecycleGraphExtras, %ARGS &>
         <g class="transitions"></g>
         <g class="statuses"></g>
         <g class="decorations"></g>
diff --git a/html/Elements/LifecycleGraphExtras b/html/Elements/LifecycleGraphExtras
new file mode 100644
index 0000000..000a62d
--- /dev/null
+++ b/html/Elements/LifecycleGraphExtras
@@ -0,0 +1,5 @@
+<defs>
+  <marker id="marker_arrowhead" markerHeight=5 markerWidth=5 markerUnits="strokeWidth" orient="auto" refX=40 refY=0 viewBox="-5 -5 10 10">
+    <path d="M 0,0 m -5,-5 L 5,0 L -5,5 Z" fill="black" />
+  </marker>
+</defs>
diff --git a/static/js/lifecycleui-viewer.js b/static/js/lifecycleui-viewer.js
index 719a0d4..6f432c8 100644
--- a/static/js/lifecycleui-viewer.js
+++ b/static/js/lifecycleui-viewer.js
@@ -5,22 +5,6 @@ jQuery(function () {
         this.padding = this.statusCircleRadius;
     };
 
-    Viewer.prototype.createArrowHead = function () {
-        var defs = this.svg.append('defs');
-        defs.append('marker')
-            .attr('id', 'marker_arrowhead')
-            .attr('markerHeight', 5)
-            .attr('markerWidth', 5)
-            .attr('markerUnits', 'strokeWidth')
-            .attr('orient', 'auto')
-            .attr('refX', this.statusCircleRadius + 5)
-            .attr('refY', 0)
-            .attr('viewBox', '-5 -5 10 10')
-            .append('path')
-              .attr('d', 'M 0,0 m -5,-5 L 5,0 L -5,5 Z')
-              .attr('fill', 'black');
-    };
-
     Viewer.prototype.createScale = function (size, padding) {
         return d3.scaleLinear()
                  .domain([0, 1])
@@ -254,7 +238,6 @@ jQuery(function () {
         self.lifecycle = new RT.Lifecycle();
         self.lifecycle.initializeFromConfig(config);
 
-        self.createArrowHead();
         self.addZoomBehavior();
 
         self.focusOnStatus(focusStatus);

commit d083e81808e65470b3b72090a92e5359fb5c7958
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 14:39:11 2017 +0000

    Focus on status to adjust opacity of irrelevant statuses/transitions

diff --git a/static/css/lifecycleui-viewer.css b/static/css/lifecycleui-viewer.css
index 13dac4e..3af3f9f 100644
--- a/static/css/lifecycleui-viewer.css
+++ b/static/css/lifecycleui-viewer.css
@@ -45,3 +45,23 @@
     background: none;
 }
 
+.lifecycle-ui .has-focus .statuses > * {
+    opacity: .15;
+}
+
+.lifecycle-ui .has-focus .transitions > * {
+    opacity: 0;
+}
+
+.lifecycle-ui .has-focus .statuses .focus {
+    opacity: 1;
+}
+
+.lifecycle-ui .has-focus .statuses .focus-to,
+.lifecycle-ui .has-focus .transitions .focus-to {
+    opacity: 1;
+}
+
+.lifecycle-ui .has-focus .transitions .focus-from {
+    opacity: .15;
+}
diff --git a/static/js/lifecycleui-viewer.js b/static/js/lifecycleui-viewer.js
index 6f432c8..dc90869 100644
--- a/static/js/lifecycleui-viewer.js
+++ b/static/js/lifecycleui-viewer.js
@@ -64,7 +64,10 @@ jQuery(function () {
                 .merge(statuses)
                         .attr("cx", function (d) { return self.xScale(d.x) })
                         .attr("cy", function (d) { return self.yScale(d.y) })
-                        .attr("fill", function (d) { return d.color });
+                        .attr("fill", function (d) { return d.color })
+                        .classed("focus", function (d) { return self.focusStatus == d.name })
+                        .classed("focus-from", function (d) { return self.lifecycle.hasTransition(d.name, self.focusStatus) })
+                        .classed("focus-to", function (d) { return self.lifecycle.hasTransition(self.focusStatus, d.name) });
     };
 
     Viewer.prototype.clickedStatus = function (d) { };
@@ -104,6 +107,9 @@ jQuery(function () {
                       .attr("y", function (d) { return self.yScale(d.y) })
                       .attr("fill", function (d) { return d3.hsl(d.color).l > 0.35 ? '#000' : '#fff' })
                       .text(function (d) { return d.name }).each(function () { self.truncateLabel(this) })
+                      .classed("focus", function (d) { return self.focusStatus == d.name })
+                      .classed("focus-from", function (d) { return self.lifecycle.hasTransition(d.name, self.focusStatus) })
+                      .classed("focus-to", function (d) { return self.lifecycle.hasTransition(self.focusStatus, d.name) });
     };
 
     Viewer.prototype.transitionArc = function (d) {
@@ -136,6 +142,8 @@ jQuery(function () {
                       .attr("d", function (d) { return self.transitionArc(d) })
                       .classed("dashed", function (d) { return d.style == 'dashed' })
                       .classed("dotted", function (d) { return d.style == 'dotted' })
+                      .classed("focus-from", function (d) { return self.focusStatus == d.to })
+                      .classed("focus-to", function (d) { return self.focusStatus == d.from });
     };
 
     Viewer.prototype.renderTextDecorations = function (initial) {
@@ -210,13 +218,18 @@ jQuery(function () {
                 .call(this._zoom.translateTo, x, y);
     };
 
-    Viewer.prototype.focusOnStatus = function (statusName) {
+    Viewer.prototype.focusOnStatus = function (statusName, center) {
         if (!statusName) {
             return;
         }
 
-        var meta = this.lifecycle.statusObjectForName(statusName);
-        this.centerOnItem(meta);
+        this.focusStatus = statusName;
+        this.svg.classed("has-focus", true);
+
+        if (center) {
+            var meta = this.lifecycle.statusObjectForName(statusName);
+            this.centerOnItem(meta);
+        }
     };
 
     Viewer.prototype.initializeViewer = function (node, config, focusStatus) {
@@ -240,7 +253,7 @@ jQuery(function () {
 
         self.addZoomBehavior();
 
-        self.focusOnStatus(focusStatus);
+        self.focusOnStatus(focusStatus, true);
 
         self.renderDisplay(true);
     };

commit a963f89bef9f5f435033bfffa5ac37d9685b5f61
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 14:40:32 2017 +0000

    Have focus render a drop shadow

diff --git a/html/Elements/LifecycleGraphExtras b/html/Elements/LifecycleGraphExtras
index 000a62d..15df313 100644
--- a/html/Elements/LifecycleGraphExtras
+++ b/html/Elements/LifecycleGraphExtras
@@ -3,3 +3,12 @@
     <path d="M 0,0 m -5,-5 L 5,0 L -5,5 Z" fill="black" />
   </marker>
 </defs>
+
+<filter id="focus" height="150%">
+  <feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
+  <feOffset dx="0" dy="0" result="offsetblur"/>
+  <feMerge>
+    <feMergeNode/>
+    <feMergeNode in="SourceGraphic"/>
+  </feMerge>
+</filter>
diff --git a/static/css/lifecycleui-viewer.css b/static/css/lifecycleui-viewer.css
index 3af3f9f..df4782c 100644
--- a/static/css/lifecycleui-viewer.css
+++ b/static/css/lifecycleui-viewer.css
@@ -57,6 +57,10 @@
     opacity: 1;
 }
 
+.lifecycle-ui .has-focus .statuses circle.focus {
+    filter: url(#focus);
+}
+
 .lifecycle-ui .has-focus .statuses .focus-to,
 .lifecycle-ui .has-focus .transitions .focus-to {
     opacity: 1;

commit 7088166bdb9e4f2be48a3279fa60a6c043fcad80
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 15:20:33 2017 +0000

    Remove accidentally-committed console.log

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 7a25693..f7fc5eb 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -55,7 +55,6 @@ jQuery(function () {
         inspector.find(':checkbox[data-show-hide]').each(function () {
             var field = jQuery(this);
             var selector = field.data('show-hide');
-            console.log(field, selector);
             var toggle = function () {
                 if (field.prop('checked')) {
                     jQuery(selector).show();

commit 3fff1d3a0fb058a7ee910f67f36b53a1188344a8
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 15:26:35 2017 +0000

    Refactor focus/selection/highlighting system

diff --git a/static/css/lifecycleui-editor.css b/static/css/lifecycleui-editor.css
index 0575c64..2a57b23 100644
--- a/static/css/lifecycleui-editor.css
+++ b/static/css/lifecycleui-editor.css
@@ -21,18 +21,6 @@
     border: 1px solid black;
 }
 
-.lifecycle-ui .statuses circle,
-.lifecycle-ui .statuses text,
-.lifecycle-ui .transitions path,
-.lifecycle-ui .selection .decorations > *,
-.lifecycle-ui .removing {
-    transition: opacity .2s;
-}
-
-.lifecycle-ui .statuses circle.selected {
-    stroke-width: 6px;
-}
-
 .lifecycle-ui .inspector .color-control span.current-color {
     display: inline;
     padding-left: 1em;
@@ -43,40 +31,7 @@
     opacity: 0 !important;
 }
 
-.lifecycle-ui .selection .statuses circle,
-.lifecycle-ui .selection .statuses text {
-    opacity: 0.25;
-}
-
-.lifecycle-ui .selection .statuses circle.reachable,
-.lifecycle-ui .selection .statuses circle.selected,
-.lifecycle-ui .selection .statuses circle.selected-source,
-.lifecycle-ui .selection .statuses circle.selected-sink,
-.lifecycle-ui .selection .statuses text.reachable,
-.lifecycle-ui .selection .statuses text.selected,
-.lifecycle-ui .selection .statuses text.selected-source,
-.lifecycle-ui .selection .statuses text.selected-sink {
-    opacity: 1;
-}
-
-.lifecycle-ui .selection .transitions path {
-    opacity: 0;
-}
-
-.lifecycle-ui .selection .transitions path.selected {
-    opacity: 1;
-}
-
-.lifecycle-ui .selection .decorations > * {
-    opacity: .2;
-}
-
-.lifecycle-ui .selection .decorations > .selected,
-.lifecycle-ui .selection .decorations > .point-handle {
-    opacity: 1;
-}
-
-.lifecycle-ui .selection .point-handle {
+.lifecycle-ui .has-focus .point-handle {
     stroke: black;
     fill: steelblue;
     r: 5px;
@@ -109,3 +64,17 @@
     margin-bottom: .5em;
     padding: .5em;
 }
+
+.lifecycle-ui svg.editing.has-focus .decorations > * {
+    opacity: .15;
+}
+
+.lifecycle-ui svg.editing.has-focus .decorations .focus,
+.lifecycle-ui svg.editing.has-focus .decorations .point-handle,
+.lifecycle-ui svg.editing.has-focus .transitions .focus {
+    opacity: 1;
+}
+
+.lifecycle-ui svg.editing.has-focus[data-focus-type=transition] .statuses .focus-from {
+    opacity: 1;
+}
diff --git a/static/css/lifecycleui-viewer.css b/static/css/lifecycleui-viewer.css
index df4782c..87e4b31 100644
--- a/static/css/lifecycleui-viewer.css
+++ b/static/css/lifecycleui-viewer.css
@@ -45,6 +45,12 @@
     background: none;
 }
 
+.lifecycle-ui .statuses > *,
+.lifecycle-ui .transitions > *,
+.lifecycle-ui .decorations > * {
+    transition: opacity .2s;
+}
+
 .lifecycle-ui .has-focus .statuses > * {
     opacity: .15;
 }
@@ -58,6 +64,12 @@
 }
 
 .lifecycle-ui .has-focus .statuses circle.focus {
+    stroke-width: 6px;
+}
+
+.lifecycle-ui .has-focus .statuses circle.focus,
+.lifecycle-ui .has-focus .transitions .focus,
+.lifecycle-ui .has-focus .decorations :not(text).focus {
     filter: url(#focus);
 }
 
@@ -66,6 +78,3 @@
     opacity: 1;
 }
 
-.lifecycle-ui .has-focus .transitions .focus-from {
-    opacity: .15;
-}
diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index f7fc5eb..c7e4118 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -202,17 +202,15 @@ jQuery(function () {
 
     Editor.prototype.deselectAll = function (clearSelection) {
         var svg = this.svg;
-        svg.classed('selection', false);
-        svg.selectAll('.selected').classed('selected', false);
-        svg.selectAll('.selected-source').classed('selected-source', false);
-        svg.selectAll('.selected-sink').classed('selected-sink', false);
-        svg.selectAll('.reachable').classed('reachable', false);
 
         this.removePointHandles();
 
         if (clearSelection) {
             this.setInspectorContent(null);
         }
+
+        this.defocus();
+        this.renderDisplay();
     };
 
     Editor.prototype.selectStatus = function (name) {
@@ -220,17 +218,9 @@ jQuery(function () {
         var d = self.lifecycle.statusObjectForName(name);
 
         self.deselectAll(false);
-
-        self.svg.classed('selection', true);
-        self.statusContainer.selectAll('*[data-key="'+d._key+'"]').classed('selected', true);
-
-        jQuery.each(self.lifecycle.transitionsFrom(name), function (i, transition) {
-            var key = self.lifecycle.keyForStatusName(transition.to);
-            self.statusContainer.selectAll('*[data-key="'+key+'"]').classed('reachable', true);
-            self.transitionContainer.selectAll('path[data-key="'+transition._key+'"]').classed('selected', true);
-        });
-
+        self.focusItem(d);
         self.setInspectorContent(d);
+        self.renderDisplay();
     };
 
     Editor.prototype.selectTransition = function (fromStatus, toStatus) {
@@ -238,25 +228,16 @@ jQuery(function () {
         var d = self.lifecycle.hasTransition(fromStatus, toStatus);
 
         self.deselectAll(false);
-
-        self.svg.classed('selection', true);
-
-        var fromKey = self.lifecycle.keyForStatusName(fromStatus);
-        var toKey = self.lifecycle.keyForStatusName(toStatus);
-        self.statusContainer.selectAll('*[data-key="'+fromKey+'"]').classed('selected-source', true);
-        self.statusContainer.selectAll('*[data-key="'+toKey+'"]').classed('selected-sink', true);
-        self.transitionContainer.select('path[data-key="'+d._key+'"]').classed('selected', true);
-
+        self.focusItem(d);
         self.setInspectorContent(d);
+        self.renderDisplay();
     };
 
     Editor.prototype.selectDecoration = function (key) {
         var d = this.lifecycle.itemForKey(key);
 
         this.deselectAll(false);
-
-        this.svg.classed('selection', true);
-        this.decorationContainer.selectAll('*[data-key="'+key+'"]').classed('selected', true);
+        this.focusItem(d);
         this.setInspectorContent(d);
 
         if (d._type == 'polygon') {
diff --git a/static/js/lifecycleui-viewer.js b/static/js/lifecycleui-viewer.js
index dc90869..325b841 100644
--- a/static/js/lifecycleui-viewer.js
+++ b/static/js/lifecycleui-viewer.js
@@ -65,9 +65,9 @@ jQuery(function () {
                         .attr("cx", function (d) { return self.xScale(d.x) })
                         .attr("cy", function (d) { return self.yScale(d.y) })
                         .attr("fill", function (d) { return d.color })
-                        .classed("focus", function (d) { return self.focusStatus == d.name })
-                        .classed("focus-from", function (d) { return self.lifecycle.hasTransition(d.name, self.focusStatus) })
-                        .classed("focus-to", function (d) { return self.lifecycle.hasTransition(self.focusStatus, d.name) });
+                        .classed("focus", function (d) { return self.isFocused(d) })
+                        .classed("focus-from", function (d) { return self.isFocusedTransition(d, true) })
+                        .classed("focus-to", function (d) { return self.isFocusedTransition(d, false) });
     };
 
     Viewer.prototype.clickedStatus = function (d) { };
@@ -107,9 +107,9 @@ jQuery(function () {
                       .attr("y", function (d) { return self.yScale(d.y) })
                       .attr("fill", function (d) { return d3.hsl(d.color).l > 0.35 ? '#000' : '#fff' })
                       .text(function (d) { return d.name }).each(function () { self.truncateLabel(this) })
-                      .classed("focus", function (d) { return self.focusStatus == d.name })
-                      .classed("focus-from", function (d) { return self.lifecycle.hasTransition(d.name, self.focusStatus) })
-                      .classed("focus-to", function (d) { return self.lifecycle.hasTransition(self.focusStatus, d.name) });
+                      .classed("focus", function (d) { return self.isFocused(d) })
+                      .classed("focus-from", function (d) { return self.isFocusedTransition(d, true) })
+                      .classed("focus-to", function (d) { return self.isFocusedTransition(d, false) });
     };
 
     Viewer.prototype.transitionArc = function (d) {
@@ -142,8 +142,9 @@ jQuery(function () {
                       .attr("d", function (d) { return self.transitionArc(d) })
                       .classed("dashed", function (d) { return d.style == 'dashed' })
                       .classed("dotted", function (d) { return d.style == 'dotted' })
-                      .classed("focus-from", function (d) { return self.focusStatus == d.to })
-                      .classed("focus-to", function (d) { return self.focusStatus == d.from });
+                      .classed("focus", function (d) { return self.isFocused(d) })
+                      .classed("focus-from", function (d) { return self.isFocusedTransition(d, true) })
+                      .classed("focus-to", function (d) { return self.isFocusedTransition(d, false) });
     };
 
     Viewer.prototype.renderTextDecorations = function (initial) {
@@ -166,7 +167,8 @@ jQuery(function () {
               .merge(labels)
                       .attr("x", function (d) { return self.xScale(d.x) })
                       .attr("y", function (d) { return self.yScale(d.y) })
-                      .text(function (d) { return d.text });
+                      .text(function (d) { return d.text })
+                      .classed("focus", function (d) { return self.isFocused(d) })
     };
 
     Viewer.prototype.renderPolygonDecorations = function (initial) {
@@ -188,15 +190,16 @@ jQuery(function () {
                      .call(function (polygons) { self.didEnterPolygonDecorations(polygons) })
               .merge(polygons)
                      .attr("stroke", function (d) { return d.renderStroke ? d.stroke : 'none' })
-                      .classed("dashed", function (d) { return d.strokeStyle == 'dashed' })
-                      .classed("dotted", function (d) { return d.strokeStyle == 'dotted' })
+                     .classed("dashed", function (d) { return d.strokeStyle == 'dashed' })
+                     .classed("dotted", function (d) { return d.strokeStyle == 'dotted' })
                      .attr("fill", function (d) { return d.renderFill ? d.fill : 'none' })
                      .attr("transform", function (d) { return "translate(" + self.xScale(d.x) + ", " + self.yScale(d.y) + ")" })
                      .attr("points", function (d) {
                          return jQuery.map(d.points, function(p) {
                              return [self.xScale(p.x),self.yScale(p.y)].join(",");
                          }).join(" ");
-                     });
+                     })
+                    .classed("focus", function (d) { return self.isFocused(d) })
     };
 
     Viewer.prototype.renderDecorations = function (initial) {
@@ -218,18 +221,73 @@ jQuery(function () {
                 .call(this._zoom.translateTo, x, y);
     };
 
+    Viewer.prototype.defocus = function () {
+        this._focusItem = null;
+        this.svg.classed("has-focus", false)
+                .attr('data-focus-type', undefined);
+    };
+
+    Viewer.prototype.focusItem = function (d) {
+        this._focusItem = d;
+        this.svg.classed("has-focus", true)
+                .attr('data-focus-type', d._type);
+    };
+
     Viewer.prototype.focusOnStatus = function (statusName, center) {
         if (!statusName) {
             return;
         }
 
-        this.focusStatus = statusName;
-        this.svg.classed("has-focus", true);
+        var meta = this.lifecycle.statusObjectForName(statusName);
+        this.focusItem(meta);
 
         if (center) {
-            var meta = this.lifecycle.statusObjectForName(statusName);
-            this.centerOnItem(meta);
+            this.centerOnItem(meta)
+        }
+    };
+
+    Viewer.prototype.isFocused = function (d) {
+        if (!this._focusItem) {
+            return false;
         }
+        return this._focusItem._key == d._key;
+    };
+
+    Viewer.prototype.isFocusedTransition = function (d, isFrom) {
+        if (!this._focusItem) {
+            return false;
+        }
+
+        if (d._type == 'status') {
+            if (this._focusItem._type == 'status') {
+                if (isFrom) {
+                    return this.lifecycle.hasTransition(d.name, this._focusItem.name);
+                }
+                else {
+                    return this.lifecycle.hasTransition(this._focusItem.name, d.name);
+                }
+            }
+            else if (this._focusItem._type == 'transition') {
+                if (isFrom) {
+                    return this._focusItem.from == d.name;
+                }
+                else {
+                    return this._focusItem.to == d.name;
+                }
+            }
+        }
+        else if (d._type == 'transition') {
+            if (this._focusItem._type == 'status') {
+                if (isFrom) {
+                    return d.to == this._focusItem.name;
+                }
+                else {
+                    return d.from == this._focusItem.name;
+                }
+            }
+        }
+
+        return false;
     };
 
     Viewer.prototype.initializeViewer = function (node, config, focusStatus) {

commit 8d8396b3329ad413bc04000789a7d884ab089e5e
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 15:34:54 2017 +0000

    Hide stroke style selector when renderStroke unchecked

diff --git a/html/Elements/LifecycleInspectorPolygon b/html/Elements/LifecycleInspectorPolygon
index 2285fcf..dbdb168 100644
--- a/html/Elements/LifecycleInspectorPolygon
+++ b/html/Elements/LifecycleInspectorPolygon
@@ -1,13 +1,14 @@
 <script type="text/x-template" class="lifecycle-inspector-template" data-type="polygon">
     <div class="polygon">
-        Stroke: <input type="checkbox" name="renderStroke" {{#if polygon.renderStroke}}checked=checked{{/if}} data-show-hide=".color-control[data-field=stroke]"> <span class="color-control" data-field="stroke"><span class="current-color" title="{{polygon.stroke}}" style="background-color: {{polygon.stroke}}"> </span> <button class="change-color"><&|/l&>Change</&></button></span><br>
-       Style: <select name="strokeStyle">
+       Stroke: <input type="checkbox" name="renderStroke" {{#if polygon.renderStroke}}checked=checked{{/if}} data-show-hide=".color-control[data-field=stroke], .stroke-style"> <span class="color-control" data-field="stroke"><span class="current-color" title="{{polygon.stroke}}" style="background-color: {{polygon.stroke}}"> </span> <button class="change-color"><&|/l&>Change</&></button></span><br>
+       <div class="stroke-style">Style: <select name="strokeStyle">
                  {{#select polygon.strokeStyle}}
                  <option value="solid"><&|/l&>solid</&></option>
                  <option value="dashed"><&|/l&>dashed</&></option>
                  <option value="dotted"><&|/l&>dotted</&></option>
                  {{/select}}
-             </select><br>
+             </select>
+        </div>
         Fill: <input type="checkbox" name="renderFill" {{#if polygon.renderFill}}checked=checked{{/if}} data-show-hide=".color-control[data-field=fill]"> <span class="color-control" data-field="fill"><span class="current-color" title="{{polygon.fill}}" style="background-color: {{polygon.fill}}"> </span> <button class="change-color"><&|/l&>Change</&></button></span><br>
         <button class="delete"><&|/l&>Delete Polygon</&></button>
     </div>

commit d956e9236e4e38c4ab022c5bbe632406a4fbb9f3
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 15:35:27 2017 +0000

    Add label for polygons

diff --git a/html/Elements/LifecycleInspectorCanvas b/html/Elements/LifecycleInspectorCanvas
index f5d091f..9ee5814 100644
--- a/html/Elements/LifecycleInspectorCanvas
+++ b/html/Elements/LifecycleInspectorCanvas
@@ -6,8 +6,8 @@
         <ul class="toplevel sf-menu sf-vertical sf-js-enabled sf-shadow">
           <li class="has-children">Add Shape
               <ul>
-                  <li><a href="#" class="add-polygon" data-type="triangle">Add Triangle</a></li>
-                  <li><a href="#" class="add-polygon" data-type="rectangle">Add Rectangle</a></li>
+                  <li><a href="#" class="add-polygon" data-type="Triangle">Add Triangle</a></li>
+                  <li><a href="#" class="add-polygon" data-type="Rectangle">Add Rectangle</a></li>
               </ul>
           </li>
           <li class="has-children">Select Status
diff --git a/html/Elements/LifecycleInspectorPolygon b/html/Elements/LifecycleInspectorPolygon
index dbdb168..9c645e2 100644
--- a/html/Elements/LifecycleInspectorPolygon
+++ b/html/Elements/LifecycleInspectorPolygon
@@ -1,5 +1,6 @@
 <script type="text/x-template" class="lifecycle-inspector-template" data-type="polygon">
     <div class="polygon">
+       Label: <input type="text" name="label" value="{{polygon.label}}" /><br>
        Stroke: <input type="checkbox" name="renderStroke" {{#if polygon.renderStroke}}checked=checked{{/if}} data-show-hide=".color-control[data-field=stroke], .stroke-style"> <span class="color-control" data-field="stroke"><span class="current-color" title="{{polygon.stroke}}" style="background-color: {{polygon.stroke}}"> </span> <button class="change-color"><&|/l&>Change</&></button></span><br>
        <div class="stroke-style">Style: <select name="strokeStyle">
                  {{#select polygon.strokeStyle}}
diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index 3992d18..ec5ffa4 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -13,12 +13,12 @@ jQuery(function () {
         this._statusMeta = {};
 
         this._initialPointsForPolygon = {
-            triangle: [
+            Triangle: [
                 {x:  .07, y: .2},
                 {x:    0, y:  0},
                 {x: -.06, y: .2}
             ],
-            rectangle: [
+            Rectangle: [
                 {x: -.06, y: -.06},
                 {x:  .06, y: -.06},
                 {x:  .06, y:  .06},
@@ -558,6 +558,7 @@ jQuery(function () {
         var item = {
             _key: _ELEMENT_KEY_SEQ++,
             _type: 'polygon',
+            label: type,
             stroke: '#000000',
             renderStroke: true,
             strokeStyle: 'solid',

commit 7383f6a2e2d8449a6a2f56cf9b808f28ed23f1ab
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 15:37:00 2017 +0000

    Implement selecting decorations by menu

diff --git a/html/Elements/LifecycleInspectorCanvas b/html/Elements/LifecycleInspectorCanvas
index 9ee5814..0ebea85 100644
--- a/html/Elements/LifecycleInspectorCanvas
+++ b/html/Elements/LifecycleInspectorCanvas
@@ -17,9 +17,20 @@
               {{/each}}
               </ul>
            </li>
-           <li class="has-children">Select Transition</li>
-           <li class="has-children">Select Text</li>
-           <li class="has-children">Select Shape</li>
+           <li class="has-children">Select Text
+              <ul>
+              {{#each lifecycle.decorations.text}}
+              <li><a href="#" class="select-decoration menu-item" data-key="{{this._key}}">{{truncate this.text}}</a></li>
+              {{/each}}
+              </ul>
+           </li>
+           <li class="has-children">Select Shape
+              <ul>
+              {{#each lifecycle.decorations.polygon}}
+              <li><a href="#" class="select-decoration menu-item" data-key="{{this._key}}">{{truncate this.label}}</a></li>
+              {{/each}}
+              </ul>
+           </li>
         </ul>
     </div>
 </script>
diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index c7e4118..e38933a 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -27,6 +27,13 @@ jQuery(function () {
             return lifecycle.hasTransition(fromStatus, toStatus);
         });
 
+        Handlebars.registerHelper('truncate', function(text) {
+            if (text.length > 15) {
+                text = text.substr(0, 15) + '…';
+            }
+            return text;
+        });
+
         var templates = {};
         self.container.find('script.lifecycle-inspector-template').each(function () {
             var type = jQuery(this).data('type');
@@ -184,6 +191,12 @@ jQuery(function () {
             self.selectTransition(fromStatus, toStatus);
         });
 
+        inspector.on('click', 'a.select-decoration', function (e) {
+            e.preventDefault();
+            var key = jQuery(this).data('key');
+            self.selectDecoration(key);
+        });
+
         inspector.on('click', '.add-status', function (e) {
             e.preventDefault();
             self.addNewStatus();

commit fe68211a86fcb33be89d143a2d25c0f62c839c0d
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 15:50:49 2017 +0000

    Improve focus highlighting

diff --git a/html/Elements/LifecycleGraphExtras b/html/Elements/LifecycleGraphExtras
index 15df313..d1061c0 100644
--- a/html/Elements/LifecycleGraphExtras
+++ b/html/Elements/LifecycleGraphExtras
@@ -4,11 +4,14 @@
   </marker>
 </defs>
 
-<filter id="focus" height="150%">
-  <feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
-  <feOffset dx="0" dy="0" result="offsetblur"/>
+<filter id="focus" x="-100%" y="-100%" height="300%" width="300%">
+  <feFlood result="flood" flood-color="#FFD700" flood-opacity="1"></feFlood>
+  <feComposite in="flood" result="mask" in2="SourceGraphic" operator="in"></feComposite>
+  <feMorphology in="mask" result="dilated" operator="dilate" radius="5"></feMorphology>
+  <feGaussianBlur in="dilated" result="blurred" stdDeviation="3"></feGaussianBlur>
   <feMerge>
-    <feMergeNode/>
-    <feMergeNode in="SourceGraphic"/>
+    <feMergeNode in="blurred"></feMergeNode>
+    <feMergeNode in="SourceGraphic"></feMergeNode>
   </feMerge>
 </filter>
+

commit c1ce8d0beaa961d29d598c32fa3c6ede59b2f90e
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 16:10:20 2017 +0000

    Improve focus display for text decorations

diff --git a/static/css/lifecycleui-editor.css b/static/css/lifecycleui-editor.css
index 2a57b23..325d0f7 100644
--- a/static/css/lifecycleui-editor.css
+++ b/static/css/lifecycleui-editor.css
@@ -78,3 +78,11 @@
 .lifecycle-ui svg.editing.has-focus[data-focus-type=transition] .statuses .focus-from {
     opacity: 1;
 }
+
+.lifecycle-ui svg.editing.has-focus .decorations .text-background {
+    stroke: none;
+    fill: white;
+    opacity: 1;
+    filter: url(#focus);
+}
+
diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index e38933a..7a6c387 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -297,6 +297,38 @@ jQuery(function () {
         this.renderDisplay();
     };
 
+    // add a rect under the focused text decoration for highlighting
+    Editor.prototype.renderTextDecorations = function (initial) {
+        Super.prototype.renderTextDecorations.call(this, initial);
+        var self = this;
+
+        if (!self._focusItem || self._focusItem._type != 'text') {
+            self.decorationContainer.selectAll("rect")
+                .data([])
+                .exit()
+                .remove();
+            return;
+        }
+
+        var d = self._focusItem;
+        var label = self.decorationContainer.select("text[data-key='"+d._key+"']");
+        var rect = label.node().getBoundingClientRect();
+        var width = rect.width;
+        var height = rect.height;
+        var padding = 5;
+
+        var background = self.decorationContainer.selectAll("rect")
+                             .data([d], function (d) { return d._key });
+
+        background.enter().insert("rect", ":first-child")
+                     .classed("text-background", true)
+              .merge(background)
+                     .attr("x", self.xScale(d.x)-padding)
+                     .attr("y", self.yScale(d.y)-height-padding)
+                     .attr("width", width+padding*2)
+                     .attr("height", height+padding*2)
+    };
+
     Editor.prototype.renderPolygonDecorations = function (initial) {
         Super.prototype.renderPolygonDecorations.call(this, initial);
 

commit a1d6e94c289f7115cfecd71ee25ae8590cd25bbf
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 16:13:31 2017 +0000

    No need to call renderDisplay when we add new item

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 7a6c387..a11b519 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -400,20 +400,17 @@ jQuery(function () {
 
     Editor.prototype.addNewStatus = function () {
         var status = this.lifecycle.createStatus();
-        this.renderDisplay();
         this.selectStatus(status.name);
     };
 
     Editor.prototype.addNewTextDecoration = function () {
         var text = this.lifecycle.createTextDecoration();
-        this.renderDisplay();
         this.selectDecoration(text._key);
     };
 
     Editor.prototype.addNewPolygonDecoration = function (type) {
-        var text = this.lifecycle.createPolygonDecoration(type);
-        this.renderDisplay();
-        this.selectDecoration(text._key);
+        var polygon = this.lifecycle.createPolygonDecoration(type);
+        this.selectDecoration(polygon._key);
     };
 
     Editor.prototype.initializeEditor = function (node, config, focusStatus) {

commit 37ed7e1b2c41608e28502514d64681edd40f7f13
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 16:22:54 2017 +0000

    Implement circle decorations

diff --git a/html/Elements/LifecycleInspector b/html/Elements/LifecycleInspector
index b6a0e0a..6615613 100644
--- a/html/Elements/LifecycleInspector
+++ b/html/Elements/LifecycleInspector
@@ -6,4 +6,5 @@
 <& LifecycleInspectorTransition, %ARGS &>
 <& LifecycleInspectorText, %ARGS &>
 <& LifecycleInspectorPolygon, %ARGS &>
+<& LifecycleInspectorCircle, %ARGS &>
 <& LifecycleInspectorAction, %ARGS &>
diff --git a/html/Elements/LifecycleInspectorCanvas b/html/Elements/LifecycleInspectorCanvas
index 0ebea85..817f4b4 100644
--- a/html/Elements/LifecycleInspectorCanvas
+++ b/html/Elements/LifecycleInspectorCanvas
@@ -8,6 +8,7 @@
               <ul>
                   <li><a href="#" class="add-polygon" data-type="Triangle">Add Triangle</a></li>
                   <li><a href="#" class="add-polygon" data-type="Rectangle">Add Rectangle</a></li>
+                  <li><a href="#" class="add-circle">Add Circle</a></li>
               </ul>
           </li>
           <li class="has-children">Select Status
@@ -29,6 +30,9 @@
               {{#each lifecycle.decorations.polygon}}
               <li><a href="#" class="select-decoration menu-item" data-key="{{this._key}}">{{truncate this.label}}</a></li>
               {{/each}}
+              {{#each lifecycle.decorations.circle}}
+              <li><a href="#" class="select-decoration menu-item" data-key="{{this._key}}">{{truncate this.label}}</a></li>
+              {{/each}}
               </ul>
            </li>
         </ul>
diff --git a/html/Elements/LifecycleInspectorCircle b/html/Elements/LifecycleInspectorCircle
new file mode 100644
index 0000000..e03bf2f
--- /dev/null
+++ b/html/Elements/LifecycleInspectorCircle
@@ -0,0 +1,16 @@
+<script type="text/x-template" class="lifecycle-inspector-template" data-type="circle">
+    <div class="circle">
+       Label: <input type="text" name="label" value="{{circle.label}}" /><br>
+       Stroke: <input type="checkbox" name="renderStroke" {{#if circle.renderStroke}}checked=checked{{/if}} data-show-hide=".color-control[data-field=stroke], .stroke-style"> <span class="color-control" data-field="stroke"><span class="current-color" title="{{circle.stroke}}" style="background-color: {{circle.stroke}}"> </span> <button class="change-color"><&|/l&>Change</&></button></span><br>
+       <div class="stroke-style">Style: <select name="strokeStyle">
+                 {{#select circle.strokeStyle}}
+                 <option value="solid"><&|/l&>solid</&></option>
+                 <option value="dashed"><&|/l&>dashed</&></option>
+                 <option value="dotted"><&|/l&>dotted</&></option>
+                 {{/select}}
+             </select>
+        </div>
+        Fill: <input type="checkbox" name="renderFill" {{#if circle.renderFill}}checked=checked{{/if}} data-show-hide=".color-control[data-field=fill]"> <span class="color-control" data-field="fill"><span class="current-color" title="{{circle.fill}}" style="background-color: {{circle.fill}}"> </span> <button class="change-color"><&|/l&>Change</&></button></span><br>
+        <button class="delete"><&|/l&>Delete Circle</&></button>
+    </div>
+</script>
diff --git a/static/css/lifecycleui-viewer.css b/static/css/lifecycleui-viewer.css
index 87e4b31..8db22ef 100644
--- a/static/css/lifecycleui-viewer.css
+++ b/static/css/lifecycleui-viewer.css
@@ -30,7 +30,8 @@
     alignment-baseline: middle;
 }
 
-.lifecycle-ui .decorations polygon {
+.lifecycle-ui .decorations polygon,
+.lifecycle-ui .decorations circle {
     stroke-width: 2px;
 }
 
diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index a11b519..4773b85 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -211,6 +211,11 @@ jQuery(function () {
             e.preventDefault();
             self.addNewPolygonDecoration(jQuery(this).data('type'));
         });
+
+        inspector.on('click', '.add-circle', function (e) {
+            e.preventDefault();
+            self.addNewCircleDecoration();
+        });
     };
 
     Editor.prototype.deselectAll = function (clearSelection) {
@@ -398,6 +403,10 @@ jQuery(function () {
         polygons.call(this._createDrag());
     };
 
+    Editor.prototype.didEnterCircleDecorations = function (circles) {
+        circles.call(this._createDrag());
+    };
+
     Editor.prototype.addNewStatus = function () {
         var status = this.lifecycle.createStatus();
         this.selectStatus(status.name);
@@ -413,6 +422,11 @@ jQuery(function () {
         this.selectDecoration(polygon._key);
     };
 
+    Editor.prototype.addNewCircleDecoration = function () {
+        var circle = this.lifecycle.createCircleDecoration();
+        this.selectDecoration(circle._key);
+    };
+
     Editor.prototype.initializeEditor = function (node, config, focusStatus) {
         var self = this;
         self.initializeViewer(node, config);
diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index ec5ffa4..82cbf1b 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -173,7 +173,7 @@ jQuery(function () {
 
         self.decorations = {};
 
-        jQuery.each(['text', 'polygon'], function (i, type) {
+        jQuery.each(['text', 'polygon', 'circle'], function (i, type) {
             var decorations = [];
 
             if (config.decorations && config.decorations[type]) {
@@ -465,7 +465,7 @@ jQuery(function () {
         else if (type == 'transition') {
             this.deleteTransition(key);
         }
-        else if (type == 'text' || type == 'polygon') {
+        else if (type == 'text' || type == 'polygon' || type == 'circle') {
             this.deleteDecoration(type, key);
         }
         else {
@@ -573,6 +573,25 @@ jQuery(function () {
         return item;
     };
 
+    Lifecycle.prototype.createCircleDecoration = function () {
+        var item = {
+            _key: _ELEMENT_KEY_SEQ++,
+            _type: 'circle',
+            label: 'Circle',
+            stroke: '#000000',
+            renderStroke: true,
+            strokeStyle: 'solid',
+            fill: '#ffffff',
+            renderFill: true,
+            x: 0.5,
+            y: 0.5,
+            r: 35
+        };
+        this.decorations.circle.push(item);
+        this._keyMap[item._key] = item;
+        return item;
+    };
+
     RT.Lifecycle = Lifecycle;
 });
 
diff --git a/static/js/lifecycleui-viewer.js b/static/js/lifecycleui-viewer.js
index 325b841..2fb376c 100644
--- a/static/js/lifecycleui-viewer.js
+++ b/static/js/lifecycleui-viewer.js
@@ -41,6 +41,7 @@ jQuery(function () {
     Viewer.prototype.didEnterTransitions = function (paths) { };
     Viewer.prototype.didEnterTextDecorations = function (labels) { };
     Viewer.prototype.didEnterPolygonDecorations = function (polygons) { };
+    Viewer.prototype.didEnterCircleDecorations = function (circles) { };
 
     Viewer.prototype.renderStatusNodes = function (initial) {
         var self = this;
@@ -202,8 +203,37 @@ jQuery(function () {
                     .classed("focus", function (d) { return self.isFocused(d) })
     };
 
+    Viewer.prototype.renderCircleDecorations = function (initial) {
+        var self = this;
+        var circles = self.decorationContainer.selectAll("circle")
+                           .data(self.lifecycle.decorations.circle, function (d) { return d._key });
+
+        circles.exit()
+            .classed("removing", true)
+            .transition().duration(200)
+              .remove();
+
+        circles.enter().append("circle")
+                     .attr("data-key", function (d) { return d._key })
+                     .on("click", function (d) {
+                         d3.event.stopPropagation();
+                         self.clickedDecoration(d);
+                     })
+                     .call(function (circles) { self.didEnterCircleDecorations(circles) })
+              .merge(circles)
+                     .attr("stroke", function (d) { return d.renderStroke ? d.stroke : 'none' })
+                     .classed("dashed", function (d) { return d.strokeStyle == 'dashed' })
+                     .classed("dotted", function (d) { return d.strokeStyle == 'dotted' })
+                     .attr("fill", function (d) { return d.renderFill ? d.fill : 'none' })
+                     .attr("cx", function (d) { return self.xScale(d.x) })
+                     .attr("cy", function (d) { return self.yScale(d.y) })
+                     .attr("r", function (d) { return d.r })
+                     .classed("focus", function (d) { return self.isFocused(d) })
+    };
+
     Viewer.prototype.renderDecorations = function (initial) {
         this.renderPolygonDecorations(initial);
+        this.renderCircleDecorations(initial);
         this.renderTextDecorations(initial);
     };
 

commit e5b0a09c04bbbde8f1cd3e16f6783d2808a50030
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 16:34:26 2017 +0000

    Zero-padding scales
    
    This is meant to avoid applying padding twice when we scale relative to
    another point (e.g. scale first to place a polygon, then scale again for
    each of its component points)

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 4773b85..c744775 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -287,10 +287,10 @@ jQuery(function () {
     };
 
     Editor.prototype.didDragPointHandle = function (d, node) {
-        var x = this.xScaleInvert(d3.event.x);
-        var y = this.yScaleInvert(d3.event.y);
+        var x = this.xScaleZeroInvert(d3.event.x);
+        var y = this.yScaleZeroInvert(d3.event.y);
 
-        if (this.xScale(x) == this.xScale(d.x) && this.yScale(y) == this.yScale(d.y)) {
+        if (this.xScaleZero(x) == this.xScaleZero(d.x) && this.yScaleZero(y) == this.yScaleZero(d.y)) {
             return;
         }
 
@@ -347,13 +347,13 @@ jQuery(function () {
         handles.enter().append("circle")
                      .classed("point-handle", true)
                      .call(d3.drag()
-                         .subject(function (d) { return { x: self.xScale(d.x), y : self.yScale(d.y) } })
+                         .subject(function (d) { return { x: self.xScaleZero(d.x), y : self.yScaleZero(d.y) } })
                          .on("drag", function (d) { self.didDragPointHandle(d) })
                      )
               .merge(handles)
                      .attr("transform", function (d) { return "translate(" + self.xScale(self.inspectorNode.x) + ", " + self.yScale(self.inspectorNode.y) + ")" })
-                     .attr("cx", function (d) { return self.xScale(d.x) })
-                     .attr("cy", function (d) { return self.yScale(d.y) })
+                     .attr("cx", function (d) { return self.xScaleZero(d.x) })
+                     .attr("cy", function (d) { return self.yScaleZero(d.y) })
     };
 
     Editor.prototype.clickedStatus = function (d) {
diff --git a/static/js/lifecycleui-viewer.js b/static/js/lifecycleui-viewer.js
index 2fb376c..c3a29f2 100644
--- a/static/js/lifecycleui-viewer.js
+++ b/static/js/lifecycleui-viewer.js
@@ -14,8 +14,12 @@ jQuery(function () {
     Viewer.prototype.gridScale = function (v) { return Math.round(v/this.gridSize) * this.gridSize };
     Viewer.prototype.xScale = function (x) { return this.gridScale(this._xScale(x)) };
     Viewer.prototype.yScale = function (y) { return this.gridScale(this._yScale(y)) };
+    Viewer.prototype.xScaleZero = function (x) { return this.gridScale(this._xScaleZero(x)) };
+    Viewer.prototype.yScaleZero = function (y) { return this.gridScale(this._yScaleZero(y)) };
     Viewer.prototype.xScaleInvert = function (x) { return this._xScale.invert(x) };
     Viewer.prototype.yScaleInvert = function (y) { return this._yScale.invert(y) };
+    Viewer.prototype.xScaleZeroInvert = function (x) { return this._xScaleZero.invert(x) };
+    Viewer.prototype.yScaleZeroInvert = function (y) { return this._yScaleZero.invert(y) };
 
     Viewer.prototype.addZoomBehavior = function () {
         var self = this;
@@ -197,7 +201,7 @@ jQuery(function () {
                      .attr("transform", function (d) { return "translate(" + self.xScale(d.x) + ", " + self.yScale(d.y) + ")" })
                      .attr("points", function (d) {
                          return jQuery.map(d.points, function(p) {
-                             return [self.xScale(p.x),self.yScale(p.y)].join(",");
+                             return [self.xScaleZero(p.x),self.yScaleZero(p.y)].join(",");
                          }).join(" ");
                      })
                     .classed("focus", function (d) { return self.isFocused(d) })
@@ -335,6 +339,8 @@ jQuery(function () {
 
         self._xScale = self.createScale(self.width, self.padding);
         self._yScale = self.createScale(self.height, self.padding);
+        self._xScaleZero = self.createScale(self.width, 0);
+        self._yScaleZero = self.createScale(self.height, 0);
 
         self.lifecycle = new RT.Lifecycle();
         self.lifecycle.initializeFromConfig(config);

commit 7bf688726ebd0440c2bfd2eb7e78aa1ffe0b2c94
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 16:39:44 2017 +0000

    Differentiate between circle.decoration and circle.point-handle

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index c744775..97c98ee 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -338,7 +338,7 @@ jQuery(function () {
         Super.prototype.renderPolygonDecorations.call(this, initial);
 
         var self = this;
-        var handles = self.decorationContainer.selectAll("circle")
+        var handles = self.decorationContainer.selectAll("circle.point-handle")
                            .data(self.pointHandles || [], function (d) { return d.i });
 
         handles.exit()
diff --git a/static/js/lifecycleui-viewer.js b/static/js/lifecycleui-viewer.js
index c3a29f2..a69282d 100644
--- a/static/js/lifecycleui-viewer.js
+++ b/static/js/lifecycleui-viewer.js
@@ -209,7 +209,7 @@ jQuery(function () {
 
     Viewer.prototype.renderCircleDecorations = function (initial) {
         var self = this;
-        var circles = self.decorationContainer.selectAll("circle")
+        var circles = self.decorationContainer.selectAll("circle.decoration")
                            .data(self.lifecycle.decorations.circle, function (d) { return d._key });
 
         circles.exit()
@@ -218,6 +218,7 @@ jQuery(function () {
               .remove();
 
         circles.enter().append("circle")
+                     .classed("decoration", true)
                      .attr("data-key", function (d) { return d._key })
                      .on("click", function (d) {
                          d3.event.stopPropagation();

commit 4c659cdc6b5f1efec06fce4f37a3b9ca6b68d552
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 17:05:30 2017 +0000

    This marker is specifically for transitions

diff --git a/html/Elements/LifecycleGraphExtras b/html/Elements/LifecycleGraphExtras
index d1061c0..a5365a1 100644
--- a/html/Elements/LifecycleGraphExtras
+++ b/html/Elements/LifecycleGraphExtras
@@ -1,5 +1,5 @@
 <defs>
-  <marker id="marker_arrowhead" markerHeight=5 markerWidth=5 markerUnits="strokeWidth" orient="auto" refX=40 refY=0 viewBox="-5 -5 10 10">
+  <marker id="transition_arrowhead" markerHeight=5 markerWidth=5 markerUnits="strokeWidth" orient="auto" refX=40 refY=0 viewBox="-5 -5 10 10">
     <path d="M 0,0 m -5,-5 L 5,0 L -5,5 Z" fill="black" />
   </marker>
 </defs>
diff --git a/static/css/lifecycleui-viewer.css b/static/css/lifecycleui-viewer.css
index 8db22ef..e06421e 100644
--- a/static/css/lifecycleui-viewer.css
+++ b/static/css/lifecycleui-viewer.css
@@ -14,7 +14,7 @@
     stroke-width: 2px;
     fill: none;
     opacity: .15;
-    marker-end: url(#marker_arrowhead);
+    marker-end: url(#transition_arrowhead);
 }
 
 .lifecycle-ui .dotted {

commit 07248beddf697290e0e36601b10741d96de70c37
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 17:06:08 2017 +0000

    Implement line decorations

diff --git a/html/Elements/LifecycleGraphExtras b/html/Elements/LifecycleGraphExtras
index a5365a1..d105733 100644
--- a/html/Elements/LifecycleGraphExtras
+++ b/html/Elements/LifecycleGraphExtras
@@ -2,6 +2,9 @@
   <marker id="transition_arrowhead" markerHeight=5 markerWidth=5 markerUnits="strokeWidth" orient="auto" refX=40 refY=0 viewBox="-5 -5 10 10">
     <path d="M 0,0 m -5,-5 L 5,0 L -5,5 Z" fill="black" />
   </marker>
+  <marker id="line_marker_arrowhead" markerHeight=5 markerWidth=5 markerUnits="strokeWidth" orient="auto" refX=0 refY=0 viewBox="-5 -5 10 10">
+    <path d="M 0,0 m -5,-5 L 5,0 L -5,5 Z" fill="black" />
+  </marker>
 </defs>
 
 <filter id="focus" x="-100%" y="-100%" height="300%" width="300%">
diff --git a/html/Elements/LifecycleInspector b/html/Elements/LifecycleInspector
index 6615613..8d90b65 100644
--- a/html/Elements/LifecycleInspector
+++ b/html/Elements/LifecycleInspector
@@ -7,4 +7,5 @@
 <& LifecycleInspectorText, %ARGS &>
 <& LifecycleInspectorPolygon, %ARGS &>
 <& LifecycleInspectorCircle, %ARGS &>
+<& LifecycleInspectorLine, %ARGS &>
 <& LifecycleInspectorAction, %ARGS &>
diff --git a/html/Elements/LifecycleInspectorCanvas b/html/Elements/LifecycleInspectorCanvas
index 817f4b4..e3a3753 100644
--- a/html/Elements/LifecycleInspectorCanvas
+++ b/html/Elements/LifecycleInspectorCanvas
@@ -9,6 +9,7 @@
                   <li><a href="#" class="add-polygon" data-type="Triangle">Add Triangle</a></li>
                   <li><a href="#" class="add-polygon" data-type="Rectangle">Add Rectangle</a></li>
                   <li><a href="#" class="add-circle">Add Circle</a></li>
+                  <li><a href="#" class="add-line">Add Line</a></li>
               </ul>
           </li>
           <li class="has-children">Select Status
@@ -33,6 +34,9 @@
               {{#each lifecycle.decorations.circle}}
               <li><a href="#" class="select-decoration menu-item" data-key="{{this._key}}">{{truncate this.label}}</a></li>
               {{/each}}
+              {{#each lifecycle.decorations.line}}
+              <li><a href="#" class="select-decoration menu-item" data-key="{{this._key}}">{{truncate this.label}}</a></li>
+              {{/each}}
               </ul>
            </li>
         </ul>
diff --git a/html/Elements/LifecycleInspectorLine b/html/Elements/LifecycleInspectorLine
new file mode 100644
index 0000000..e6fea43
--- /dev/null
+++ b/html/Elements/LifecycleInspectorLine
@@ -0,0 +1,26 @@
+<script type="text/x-template" class="lifecycle-inspector-template" data-type="line">
+    <div class="line">
+       Label: <input type="text" name="label" value="{{line.label}}" /><br>
+       Start Marker: <select name="startMarker">
+                 {{#select line.startMarker}}
+                 <option value="none"><&|/l&>none</&></option>
+                 <option value="arrowhead"><&|/l&>arrowhead</&></option>
+                 {{/select}}
+                 </select><br>
+       End Marker: <select name="endMarker">
+                 {{#select line.endMarker}}
+                 <option value="none"><&|/l&>none</&></option>
+                 <option value="arrowhead"><&|/l&>arrowhead</&></option>
+                 {{/select}}
+                 </select><br>
+       Style: <select name="style">
+                 {{#select line.style}}
+                 <option value="solid"><&|/l&>solid</&></option>
+                 <option value="dashed"><&|/l&>dashed</&></option>
+                 <option value="dotted"><&|/l&>dotted</&></option>
+                 {{/select}}
+             </select><br>
+        <button class="delete"><&|/l&>Delete Line</&></button>
+    </div>
+</script>
+
diff --git a/static/css/lifecycleui-viewer.css b/static/css/lifecycleui-viewer.css
index e06421e..17b6b01 100644
--- a/static/css/lifecycleui-viewer.css
+++ b/static/css/lifecycleui-viewer.css
@@ -30,8 +30,13 @@
     alignment-baseline: middle;
 }
 
+.lifecycle-ui .decorations line {
+    stroke: #000000;
+}
+
 .lifecycle-ui .decorations polygon,
-.lifecycle-ui .decorations circle {
+.lifecycle-ui .decorations circle,
+.lifecycle-ui .decorations line {
     stroke-width: 2px;
 }
 
diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 97c98ee..ac1a652 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -216,6 +216,11 @@ jQuery(function () {
             e.preventDefault();
             self.addNewCircleDecoration();
         });
+
+        inspector.on('click', '.add-line', function (e) {
+            e.preventDefault();
+            self.addNewLineDecoration();
+        });
     };
 
     Editor.prototype.deselectAll = function (clearSelection) {
@@ -407,6 +412,10 @@ jQuery(function () {
         circles.call(this._createDrag());
     };
 
+    Editor.prototype.didEnterLineDecorations = function (lines) {
+        lines.call(this._createDrag());
+    };
+
     Editor.prototype.addNewStatus = function () {
         var status = this.lifecycle.createStatus();
         this.selectStatus(status.name);
@@ -427,6 +436,11 @@ jQuery(function () {
         this.selectDecoration(circle._key);
     };
 
+    Editor.prototype.addNewLineDecoration = function () {
+        var line = this.lifecycle.createLineDecoration();
+        this.selectDecoration(line._key);
+    };
+
     Editor.prototype.initializeEditor = function (node, config, focusStatus) {
         var self = this;
         self.initializeViewer(node, config);
diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index 82cbf1b..9eb9003 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -173,7 +173,7 @@ jQuery(function () {
 
         self.decorations = {};
 
-        jQuery.each(['text', 'polygon', 'circle'], function (i, type) {
+        jQuery.each(['text', 'polygon', 'circle', 'line'], function (i, type) {
             var decorations = [];
 
             if (config.decorations && config.decorations[type]) {
@@ -465,7 +465,7 @@ jQuery(function () {
         else if (type == 'transition') {
             this.deleteTransition(key);
         }
-        else if (type == 'text' || type == 'polygon' || type == 'circle') {
+        else if (type == 'text' || type == 'polygon' || type == 'circle' || type == 'line') {
             this.deleteDecoration(type, key);
         }
         else {
@@ -592,6 +592,24 @@ jQuery(function () {
         return item;
     };
 
+    Lifecycle.prototype.createLineDecoration = function () {
+        var item = {
+            _key: _ELEMENT_KEY_SEQ++,
+            _type: 'line',
+            label: 'Line',
+            style: 'solid',
+            startMarker: 'none',
+            endMarker: 'arrowhead',
+            x1: 0.4,
+            y1: 0.5,
+            x2: 0.6,
+            y2: 0.5
+        };
+        this.decorations.line.push(item);
+        this._keyMap[item._key] = item;
+        return item;
+    };
+
     RT.Lifecycle = Lifecycle;
 });
 
diff --git a/static/js/lifecycleui-viewer.js b/static/js/lifecycleui-viewer.js
index a69282d..01c1696 100644
--- a/static/js/lifecycleui-viewer.js
+++ b/static/js/lifecycleui-viewer.js
@@ -46,6 +46,7 @@ jQuery(function () {
     Viewer.prototype.didEnterTextDecorations = function (labels) { };
     Viewer.prototype.didEnterPolygonDecorations = function (polygons) { };
     Viewer.prototype.didEnterCircleDecorations = function (circles) { };
+    Viewer.prototype.didEnterLineDecorations = function (lines) { };
 
     Viewer.prototype.renderStatusNodes = function (initial) {
         var self = this;
@@ -236,9 +237,39 @@ jQuery(function () {
                      .classed("focus", function (d) { return self.isFocused(d) })
     };
 
+    Viewer.prototype.renderLineDecorations = function (initial) {
+        var self = this;
+        var lines = self.decorationContainer.selectAll("line")
+                           .data(self.lifecycle.decorations.line, function (d) { return d._key });
+
+        lines.exit()
+            .classed("removing", true)
+            .transition().duration(200)
+              .remove();
+
+        lines.enter().append("line")
+                     .attr("data-key", function (d) { return d._key })
+                     .on("click", function (d) {
+                         d3.event.stopPropagation();
+                         self.clickedDecoration(d);
+                     })
+                     .call(function (lines) { self.didEnterLineDecorations(lines) })
+              .merge(lines)
+                     .classed("dashed", function (d) { return d.style == 'dashed' })
+                     .classed("dotted", function (d) { return d.style == 'dotted' })
+                     .attr("x1", function (d) { return self.xScale(d.x1) })
+                     .attr("y1", function (d) { return self.yScale(d.y1) })
+                     .attr("x2", function (d) { return self.xScale(d.x2) })
+                     .attr("y2", function (d) { return self.yScale(d.y2) })
+                     .classed("focus", function (d) { return self.isFocused(d) })
+                     .attr("marker-start", function (d) { return d.startMarker == 'none' ? undefined : "url(#line_marker_" + d.startMarker + ")" })
+                     .attr("marker-end", function (d) { return d.endMarker == 'none' ? undefined : "url(#line_marker_" + d.endMarker + ")" })
+    };
+
     Viewer.prototype.renderDecorations = function (initial) {
         this.renderPolygonDecorations(initial);
         this.renderCircleDecorations(initial);
+        this.renderLineDecorations(initial);
         this.renderTextDecorations(initial);
     };
 

commit 528f3d35a8ddbb4c1f52afec5814fc755a132380
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 17:17:49 2017 +0000

    Implement diamond arrowheads

diff --git a/html/Elements/LifecycleGraphExtras b/html/Elements/LifecycleGraphExtras
index d105733..a46f56f 100644
--- a/html/Elements/LifecycleGraphExtras
+++ b/html/Elements/LifecycleGraphExtras
@@ -5,6 +5,9 @@
   <marker id="line_marker_arrowhead" markerHeight=5 markerWidth=5 markerUnits="strokeWidth" orient="auto" refX=0 refY=0 viewBox="-5 -5 10 10">
     <path d="M 0,0 m -5,-5 L 5,0 L -5,5 Z" fill="black" />
   </marker>
+  <marker id="line_marker_diamond" markerHeight=8 markerWidth=8 markerUnits="strokeWidth" orient="auto" refX=0 refY=0 viewBox="-8 -8 16 16">
+    <path d="M 0,0 m -5,0 L 0,5 L 5,0 L 0,-5 Z"  fill="white" stroke="black" stroke-width=2 stroke-dasharray="0" stroke-linecap="square" />
+  </marker>
 </defs>
 
 <filter id="focus" x="-100%" y="-100%" height="300%" width="300%">
diff --git a/html/Elements/LifecycleInspectorLine b/html/Elements/LifecycleInspectorLine
index e6fea43..26ab774 100644
--- a/html/Elements/LifecycleInspectorLine
+++ b/html/Elements/LifecycleInspectorLine
@@ -5,12 +5,14 @@
                  {{#select line.startMarker}}
                  <option value="none"><&|/l&>none</&></option>
                  <option value="arrowhead"><&|/l&>arrowhead</&></option>
+                 <option value="diamond"><&|/l&>diamond</&></option>
                  {{/select}}
                  </select><br>
        End Marker: <select name="endMarker">
                  {{#select line.endMarker}}
                  <option value="none"><&|/l&>none</&></option>
                  <option value="arrowhead"><&|/l&>arrowhead</&></option>
+                 <option value="diamond"><&|/l&>diamond</&></option>
                  {{/select}}
                  </select><br>
        Style: <select name="style">

commit 62460e54f5c2e9a8eeef4010a89f745ac1c78f59
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 17:31:05 2017 +0000

    Implement point handles for lines

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index ac1a652..d7591cf 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -263,7 +263,7 @@ jQuery(function () {
         this.focusItem(d);
         this.setInspectorContent(d);
 
-        if (d._type == 'polygon') {
+        if (d._type == 'polygon' || d._type == 'line') {
             this.addPointHandles(d);
         }
 
@@ -271,15 +271,20 @@ jQuery(function () {
     };
 
     Editor.prototype.addPointHandles = function (d) {
+        var self = this;
         var points = [];
         for (var i = 0; i < d.points.length; ++i) {
             points.push({
                 i: i,
                 x: d.points[i].x,
-                y: d.points[i].y
+                y: d.points[i].y,
+                xScale: d._type == 'polygon' ? function (v) { return self.xScaleZero(v) } : function (v) { return self.xScale(v) },
+                yScale: d._type == 'polygon' ? function (v) { return self.yScaleZero(v) } : function (v) { return self.yScale(v) },
+                xScaleInvert: d._type == 'polygon' ? function (v) { return self.xScaleZeroInvert(v) } : function (v) { return self.xScaleInvert(v) },
+                yScaleInvert: d._type == 'polygon' ? function (v) { return self.yScaleZeroInvert(v) } : function (v) { return self.yScaleInvert(v) }
             });
         }
-        this.pointHandles = points;
+        self.pointHandles = points;
     };
 
     Editor.prototype.removePointHandles = function () {
@@ -288,14 +293,14 @@ jQuery(function () {
         }
 
         delete this.pointHandles;
-        this.renderPolygonDecorations();
+        this.renderDecorations();
     };
 
     Editor.prototype.didDragPointHandle = function (d, node) {
-        var x = this.xScaleZeroInvert(d3.event.x);
-        var y = this.yScaleZeroInvert(d3.event.y);
+        var x = d.xScaleInvert(d3.event.x);
+        var y = d.yScaleInvert(d3.event.y);
 
-        if (this.xScaleZero(x) == this.xScaleZero(d.x) && this.yScaleZero(y) == this.yScaleZero(d.y)) {
+        if (d.xScale(x) == d.xScale(d.x) && d.yScale(y) == d.yScale(d.y)) {
             return;
         }
 
@@ -352,13 +357,13 @@ jQuery(function () {
         handles.enter().append("circle")
                      .classed("point-handle", true)
                      .call(d3.drag()
-                         .subject(function (d) { return { x: self.xScaleZero(d.x), y : self.yScaleZero(d.y) } })
+                         .subject(function (d) { return { x: d.xScale(d.x), y : d.yScale(d.y) } })
                          .on("drag", function (d) { self.didDragPointHandle(d) })
                      )
               .merge(handles)
                      .attr("transform", function (d) { return "translate(" + self.xScale(self.inspectorNode.x) + ", " + self.yScale(self.inspectorNode.y) + ")" })
-                     .attr("cx", function (d) { return self.xScaleZero(d.x) })
-                     .attr("cy", function (d) { return self.yScaleZero(d.y) })
+                     .attr("cx", function (d) { return d.xScale(d.x) })
+                     .attr("cy", function (d) { return d.yScale(d.y) })
     };
 
     Editor.prototype.clickedStatus = function (d) {
@@ -412,10 +417,6 @@ jQuery(function () {
         circles.call(this._createDrag());
     };
 
-    Editor.prototype.didEnterLineDecorations = function (lines) {
-        lines.call(this._createDrag());
-    };
-
     Editor.prototype.addNewStatus = function () {
         var status = this.lifecycle.createStatus();
         this.selectStatus(status.name);
diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index 9eb9003..6738732 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -600,10 +600,7 @@ jQuery(function () {
             style: 'solid',
             startMarker: 'none',
             endMarker: 'arrowhead',
-            x1: 0.4,
-            y1: 0.5,
-            x2: 0.6,
-            y2: 0.5
+            points: [{x:0.4, y:0.5}, {x:0.6, y:0.5}]
         };
         this.decorations.line.push(item);
         this._keyMap[item._key] = item;
diff --git a/static/js/lifecycleui-viewer.js b/static/js/lifecycleui-viewer.js
index 01c1696..5443a49 100644
--- a/static/js/lifecycleui-viewer.js
+++ b/static/js/lifecycleui-viewer.js
@@ -257,10 +257,10 @@ jQuery(function () {
               .merge(lines)
                      .classed("dashed", function (d) { return d.style == 'dashed' })
                      .classed("dotted", function (d) { return d.style == 'dotted' })
-                     .attr("x1", function (d) { return self.xScale(d.x1) })
-                     .attr("y1", function (d) { return self.yScale(d.y1) })
-                     .attr("x2", function (d) { return self.xScale(d.x2) })
-                     .attr("y2", function (d) { return self.yScale(d.y2) })
+                     .attr("x1", function (d) { return self.xScale(d.points[0].x) })
+                     .attr("y1", function (d) { return self.yScale(d.points[0].y) })
+                     .attr("x2", function (d) { return self.xScale(d.points[1].x) })
+                     .attr("y2", function (d) { return self.yScale(d.points[1].y) })
                      .classed("focus", function (d) { return self.isFocused(d) })
                      .attr("marker-start", function (d) { return d.startMarker == 'none' ? undefined : "url(#line_marker_" + d.startMarker + ")" })
                      .attr("marker-end", function (d) { return d.endMarker == 'none' ? undefined : "url(#line_marker_" + d.endMarker + ")" })

commit fb1bf8b37b1f8ba954c32efd007dcfcb3f343595
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 17:32:20 2017 +0000

    Move line point handles off the arrowheads

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index d7591cf..fb85e18 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -361,7 +361,7 @@ jQuery(function () {
                          .on("drag", function (d) { self.didDragPointHandle(d) })
                      )
               .merge(handles)
-                     .attr("transform", function (d) { return "translate(" + self.xScale(self.inspectorNode.x) + ", " + self.yScale(self.inspectorNode.y) + ")" })
+                     .attr("transform", function (d) { return self.inspectorNode._type == 'polygon' ? "translate(" + self.xScale(self.inspectorNode.x) + ", " + self.yScale(self.inspectorNode.y) + ")" : 'translate(0, 20)'})
                      .attr("cx", function (d) { return d.xScale(d.x) })
                      .attr("cy", function (d) { return d.yScale(d.y) })
     };

commit b49bb8d5e18e04c94d331eeb23b39e9bfa77d4ec
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 17:38:23 2017 +0000

    Render statuses above decorations

diff --git a/html/Elements/LifecycleGraph b/html/Elements/LifecycleGraph
index 9c62aef..6da1835 100644
--- a/html/Elements/LifecycleGraph
+++ b/html/Elements/LifecycleGraph
@@ -1,9 +1,9 @@
 <div class="lifecycle-ui" id="lifecycle-<% $id %>">
     <svg<% $Editing ? ' class="editing"' : '' |n %>>
         <& /Elements/LifecycleGraphExtras, %ARGS &>
+        <g class="decorations"></g>
         <g class="transitions"></g>
         <g class="statuses"></g>
-        <g class="decorations"></g>
     </svg>
 % if ($Editing) {
     <& /Elements/LifecycleInspector &>

commit 63df0d2a85d676aee5c511f72c86b37d4e358050
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 17:41:48 2017 +0000

    Unify shapes and text in the UI as decorations

diff --git a/html/Elements/LifecycleInspectorCanvas b/html/Elements/LifecycleInspectorCanvas
index e3a3753..03d728d 100644
--- a/html/Elements/LifecycleInspectorCanvas
+++ b/html/Elements/LifecycleInspectorCanvas
@@ -1,11 +1,11 @@
 <script type="text/x-template" class="lifecycle-inspector-template" data-type="canvas">
     <div class="canvas">
         <button class="add-status"><&|/l&>Add Status</&></button><br>
-        <button class="add-text"><&|/l&>Add Text</&></button><br>
     <br>
         <ul class="toplevel sf-menu sf-vertical sf-js-enabled sf-shadow">
-          <li class="has-children">Add Shape
+          <li class="has-children">Add Decoration
               <ul>
+                  <li><a href="#" class="add-text">Add Text</a></li>
                   <li><a href="#" class="add-polygon" data-type="Triangle">Add Triangle</a></li>
                   <li><a href="#" class="add-polygon" data-type="Rectangle">Add Rectangle</a></li>
                   <li><a href="#" class="add-circle">Add Circle</a></li>
@@ -19,15 +19,11 @@
               {{/each}}
               </ul>
            </li>
-           <li class="has-children">Select Text
+           <li class="has-children">Select Decoration
               <ul>
               {{#each lifecycle.decorations.text}}
               <li><a href="#" class="select-decoration menu-item" data-key="{{this._key}}">{{truncate this.text}}</a></li>
               {{/each}}
-              </ul>
-           </li>
-           <li class="has-children">Select Shape
-              <ul>
               {{#each lifecycle.decorations.polygon}}
               <li><a href="#" class="select-decoration menu-item" data-key="{{this._key}}">{{truncate this.label}}</a></li>
               {{/each}}

commit 4cbb0fe92e67328d409f1be8aeadb4829eb73501
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 17:44:59 2017 +0000

    Don't allow dragging an item different than the selected item
    
    but dragging while there's no selected item is OK too

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index fb85e18..fcb2b28 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -379,6 +379,10 @@ jQuery(function () {
     };
 
     Editor.prototype.didDragItem = function (d, node) {
+        if (this.inspectorNode && this.inspectorNode._key != d._key) {
+            return;
+        }
+
         var x = this.xScaleInvert(d3.event.x);
         var y = this.yScaleInvert(d3.event.y);
 

commit 12381fa94f081fb958a2fd2c14a2d89d25595524
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 17:45:51 2017 +0000

    Rename Stroke to Border in UI

diff --git a/html/Elements/LifecycleInspectorCircle b/html/Elements/LifecycleInspectorCircle
index e03bf2f..43ddca8 100644
--- a/html/Elements/LifecycleInspectorCircle
+++ b/html/Elements/LifecycleInspectorCircle
@@ -1,7 +1,7 @@
 <script type="text/x-template" class="lifecycle-inspector-template" data-type="circle">
     <div class="circle">
        Label: <input type="text" name="label" value="{{circle.label}}" /><br>
-       Stroke: <input type="checkbox" name="renderStroke" {{#if circle.renderStroke}}checked=checked{{/if}} data-show-hide=".color-control[data-field=stroke], .stroke-style"> <span class="color-control" data-field="stroke"><span class="current-color" title="{{circle.stroke}}" style="background-color: {{circle.stroke}}"> </span> <button class="change-color"><&|/l&>Change</&></button></span><br>
+       Border: <input type="checkbox" name="renderStroke" {{#if circle.renderStroke}}checked=checked{{/if}} data-show-hide=".color-control[data-field=stroke], .stroke-style"> <span class="color-control" data-field="stroke"><span class="current-color" title="{{circle.stroke}}" style="background-color: {{circle.stroke}}"> </span> <button class="change-color"><&|/l&>Change</&></button></span><br>
        <div class="stroke-style">Style: <select name="strokeStyle">
                  {{#select circle.strokeStyle}}
                  <option value="solid"><&|/l&>solid</&></option>
diff --git a/html/Elements/LifecycleInspectorPolygon b/html/Elements/LifecycleInspectorPolygon
index 9c645e2..0f57947 100644
--- a/html/Elements/LifecycleInspectorPolygon
+++ b/html/Elements/LifecycleInspectorPolygon
@@ -1,7 +1,7 @@
 <script type="text/x-template" class="lifecycle-inspector-template" data-type="polygon">
     <div class="polygon">
        Label: <input type="text" name="label" value="{{polygon.label}}" /><br>
-       Stroke: <input type="checkbox" name="renderStroke" {{#if polygon.renderStroke}}checked=checked{{/if}} data-show-hide=".color-control[data-field=stroke], .stroke-style"> <span class="color-control" data-field="stroke"><span class="current-color" title="{{polygon.stroke}}" style="background-color: {{polygon.stroke}}"> </span> <button class="change-color"><&|/l&>Change</&></button></span><br>
+       Border: <input type="checkbox" name="renderStroke" {{#if polygon.renderStroke}}checked=checked{{/if}} data-show-hide=".color-control[data-field=stroke], .stroke-style"> <span class="color-control" data-field="stroke"><span class="current-color" title="{{polygon.stroke}}" style="background-color: {{polygon.stroke}}"> </span> <button class="change-color"><&|/l&>Change</&></button></span><br>
        <div class="stroke-style">Style: <select name="strokeStyle">
                  {{#select polygon.strokeStyle}}
                  <option value="solid"><&|/l&>solid</&></option>

commit 7d57557fea2932cdd45ba89d1da849dddc922617
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 17:47:47 2017 +0000

    Shimmy down the delete button a bit

diff --git a/static/css/lifecycleui-editor.css b/static/css/lifecycleui-editor.css
index 325d0f7..e998341 100644
--- a/static/css/lifecycleui-editor.css
+++ b/static/css/lifecycleui-editor.css
@@ -86,3 +86,6 @@
     filter: url(#focus);
 }
 
+.lifecycle-ui .inspector button.delete {
+    margin-top: 2em;
+}

commit 90af7e5022338fb287698977b55dc4c8a2e6fc9f
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 17:59:56 2017 +0000

    Add bold and italic for text decorations

diff --git a/html/Elements/LifecycleInspectorText b/html/Elements/LifecycleInspectorText
index 7eefb8a..914ecb1 100644
--- a/html/Elements/LifecycleInspectorText
+++ b/html/Elements/LifecycleInspectorText
@@ -1,6 +1,8 @@
 <script type="text/x-template" class="lifecycle-inspector-template" data-type="text">
     <div class="text">
         Body: <input type="text" name="text" value="{{text.text}}" /><br>
+        Bold: <input type="checkbox" name="bold" {{#if text.bold}}checked=checked{{/if}}><br>
+        Italic: <input type="checkbox" name="italic" {{#if text.italic}}checked=checked{{/if}}><br>
         <button class="delete"><&|/l&>Delete Text</&></button>
     </div>
 </script>
diff --git a/static/css/lifecycleui-viewer.css b/static/css/lifecycleui-viewer.css
index 17b6b01..4dbc93d 100644
--- a/static/css/lifecycleui-viewer.css
+++ b/static/css/lifecycleui-viewer.css
@@ -84,3 +84,11 @@
     opacity: 1;
 }
 
+.lifecycle-ui .decorations text.bold {
+    font-weight: bold;
+}
+
+.lifecycle-ui .decorations text.italic {
+    font-style: italic;
+}
+
diff --git a/static/js/lifecycleui-viewer.js b/static/js/lifecycleui-viewer.js
index 5443a49..58fcb1c 100644
--- a/static/js/lifecycleui-viewer.js
+++ b/static/js/lifecycleui-viewer.js
@@ -174,6 +174,8 @@ jQuery(function () {
                       .attr("x", function (d) { return self.xScale(d.x) })
                       .attr("y", function (d) { return self.yScale(d.y) })
                       .text(function (d) { return d.text })
+                      .classed("bold", function (d) { return d.bold })
+                      .classed("italic", function (d) { return d.italic })
                       .classed("focus", function (d) { return self.isFocused(d) })
     };
 

commit 41e908546758f51cbe1d9bd66d683012469f5f5d
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 18:00:12 2017 +0000

    For now get rid of superfish menu
    
    It causes more problems than it solves

diff --git a/html/Elements/LifecycleInspectorCanvas b/html/Elements/LifecycleInspectorCanvas
index 03d728d..77fee55 100644
--- a/html/Elements/LifecycleInspectorCanvas
+++ b/html/Elements/LifecycleInspectorCanvas
@@ -1,9 +1,8 @@
 <script type="text/x-template" class="lifecycle-inspector-template" data-type="canvas">
     <div class="canvas">
         <button class="add-status"><&|/l&>Add Status</&></button><br>
-    <br>
-        <ul class="toplevel sf-menu sf-vertical sf-js-enabled sf-shadow">
-          <li class="has-children">Add Decoration
+        <ul>
+          <li>Add Decoration...
               <ul>
                   <li><a href="#" class="add-text">Add Text</a></li>
                   <li><a href="#" class="add-polygon" data-type="Triangle">Add Triangle</a></li>
@@ -12,26 +11,26 @@
                   <li><a href="#" class="add-line">Add Line</a></li>
               </ul>
           </li>
-          <li class="has-children">Select Status
+          <li>Select Status...
               <ul>
               {{#each lifecycle.statuses}}
-              <li><a href="#" class="select-status menu-item" data-name="{{this}}">{{this}}</a></li>
+              <li><a href="#" class="select-status" data-name="{{this}}">{{this}}</a></li>
               {{/each}}
               </ul>
            </li>
-           <li class="has-children">Select Decoration
+           <li>Select Decoration...
               <ul>
               {{#each lifecycle.decorations.text}}
-              <li><a href="#" class="select-decoration menu-item" data-key="{{this._key}}">{{truncate this.text}}</a></li>
+              <li><a href="#" class="select-decoration" data-key="{{this._key}}">{{truncate this.text}}</a></li>
               {{/each}}
               {{#each lifecycle.decorations.polygon}}
-              <li><a href="#" class="select-decoration menu-item" data-key="{{this._key}}">{{truncate this.label}}</a></li>
+              <li><a href="#" class="select-decoration" data-key="{{this._key}}">{{truncate this.label}}</a></li>
               {{/each}}
               {{#each lifecycle.decorations.circle}}
-              <li><a href="#" class="select-decoration menu-item" data-key="{{this._key}}">{{truncate this.label}}</a></li>
+              <li><a href="#" class="select-decoration" data-key="{{this._key}}">{{truncate this.label}}</a></li>
               {{/each}}
               {{#each lifecycle.decorations.line}}
-              <li><a href="#" class="select-decoration menu-item" data-key="{{this._key}}">{{truncate this.label}}</a></li>
+              <li><a href="#" class="select-decoration" data-key="{{this._key}}">{{truncate this.label}}</a></li>
               {{/each}}
               </ul>
            </li>
diff --git a/static/css/lifecycleui-editor.css b/static/css/lifecycleui-editor.css
index e998341..7fa87ef 100644
--- a/static/css/lifecycleui-editor.css
+++ b/static/css/lifecycleui-editor.css
@@ -37,23 +37,6 @@
     r: 5px;
 }
 
-.lifecycle-ui .inspector li.has-children {
-    background: transparent;
-}
-
-.lifecycle-ui .inspector a {
-    border: none;
-    color: #000;
-}
-
-.lifecycle-ui .inspector a.sf-with-ul {
-    padding-top: .3em;
-}
-
-.lifecycle-ui .inspector .sf-sub-indicator {
-    top: .2em;
-}
-
 .lifecycle-ui .inspector .actions {
     list-style-type: none;
     padding: 0;
@@ -89,3 +72,13 @@
 .lifecycle-ui .inspector button.delete {
     margin-top: 2em;
 }
+
+.lifecycle-ui .inspector .canvas ul {
+    list-style-type: none;
+    padding: 0;
+}
+
+.lifecycle-ui .inspector .canvas ul ul {
+    padding-left: 3em;
+    margin-bottom: 1em;
+}
diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index fcb2b28..b6ca271 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -57,7 +57,6 @@ jQuery(function () {
         params[type] = node;
 
         inspector.html(self.templates[type](params));
-        inspector.find('sf-menu').supersubs().superfish({ dropShadows: false, speed: 'fast', delay: 0 }).supposition()
 
         inspector.find(':checkbox[data-show-hide]').each(function () {
             var field = jQuery(this);

commit 750a3b93d451a7c0f4bc0f2caced160477050d30
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 18:04:44 2017 +0000

    Set constant width for inspector

diff --git a/static/css/lifecycleui-editor.css b/static/css/lifecycleui-editor.css
index 7fa87ef..febb349 100644
--- a/static/css/lifecycleui-editor.css
+++ b/static/css/lifecycleui-editor.css
@@ -15,7 +15,7 @@
 
 .lifecycle-ui .inspector {
     display: inline-block;
-    min-width: 100px;
+    width: 250px;
     padding: 10px;
     min-height: 480px;
     border: 1px solid black;

commit 9cec6553a2c964fe07f3505d7d1e36422ca08418
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 18:12:57 2017 +0000

    Use constant svg size
    
    This way when we render on ticket display, everything is same size and
    on the same grid

diff --git a/static/js/lifecycleui-viewer.js b/static/js/lifecycleui-viewer.js
index 58fcb1c..8134b94 100644
--- a/static/js/lifecycleui-viewer.js
+++ b/static/js/lifecycleui-viewer.js
@@ -1,5 +1,7 @@
 jQuery(function () {
     function Viewer (container) {
+        this.width  = 809;
+        this.height = 500;
         this.statusCircleRadius = 35;
         this.gridSize = 25;
         this.padding = this.statusCircleRadius;
@@ -368,9 +370,6 @@ jQuery(function () {
         self.statusContainer     = self.svg.select('g.statuses');
         self.decorationContainer = self.svg.select('g.decorations');
 
-        self.width  = self.svg.node().getBoundingClientRect().width;
-        self.height = self.svg.node().getBoundingClientRect().height;
-
         self._xScale = self.createScale(self.width, self.padding);
         self._yScale = self.createScale(self.height, self.padding);
         self._xScaleZero = self.createScale(self.width, 0);

commit 1cf872140af58a072c3f7c369a4e1ae42dc3c1ac
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 18:19:16 2017 +0000

    Avoid extra padding for delete action buttons

diff --git a/static/css/lifecycleui-editor.css b/static/css/lifecycleui-editor.css
index febb349..efdf7c0 100644
--- a/static/css/lifecycleui-editor.css
+++ b/static/css/lifecycleui-editor.css
@@ -73,6 +73,10 @@
     margin-top: 2em;
 }
 
+.lifecycle-ui .inspector .actions button.delete {
+    margin-top: 0;
+}
+
 .lifecycle-ui .inspector .canvas ul {
     list-style-type: none;
     padding: 0;

commit 47e12bc3a99578933f4aa85dd67dbb75db736630
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 18:23:44 2017 +0000

    Add a hint explaining the "Creation" label

diff --git a/html/Elements/LifecycleInspectorStatus b/html/Elements/LifecycleInspectorStatus
index b22e19e..8cbaceb 100644
--- a/html/Elements/LifecycleInspectorStatus
+++ b/html/Elements/LifecycleInspectorStatus
@@ -8,7 +8,7 @@
                   <option value="inactive"><&|/l&>inactive</&></option>
                   {{/select}}
               </select><br>
-        Creation: <input type="checkbox" name="creation" {{#if status.creation}}checked=checked{{/if}}><br>
+        <span title="<&|/l&>Can this status be selected on creation?</&>">Creation<span class="hint" >[?]</span>: <input type="checkbox" name="creation" {{#if status.creation}}checked=checked{{/if}}><br>
         Color: <span class="color-control" data-field="color"><span class="current-color" title="{{status.color}}" style="background-color: {{status.color}}"> </span> <button class="change-color"><&|/l&>Change</&></button></span><br>
 
         Add Transition:
diff --git a/static/css/lifecycleui-editor.css b/static/css/lifecycleui-editor.css
index efdf7c0..5e7d4b9 100644
--- a/static/css/lifecycleui-editor.css
+++ b/static/css/lifecycleui-editor.css
@@ -86,3 +86,10 @@
     padding-left: 3em;
     margin-bottom: 1em;
 }
+
+.lifecycle-ui .inspector .hint {
+    vertical-align: super;
+    font-size: .8em;
+    color: gray;
+}
+

commit d09952366ea1d23bccf3a84aabb6fc18bc02ba04
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 18:27:00 2017 +0000

    Lifecycle name

diff --git a/html/Elements/LifecycleGraph b/html/Elements/LifecycleGraph
index 6da1835..1ddd5f3 100644
--- a/html/Elements/LifecycleGraph
+++ b/html/Elements/LifecycleGraph
@@ -13,6 +13,8 @@
             jQuery(".lifecycle-ui#lifecycle-<% $id %>").each(function () {
                 var container = this;
                 var config = <% JSON($config) |n %>;
+                var name = <% $Lifecycle | j%>;
+
 % if ($Ticket) {
                 var ticketStatus = <% $Ticket->Status | j%>;
 % } else {
@@ -21,10 +23,10 @@
 
 % if ($Editing) {
                 var editor = new RT.LifecycleEditor();
-                editor.initializeEditor(container, config, ticketStatus);
+                editor.initializeEditor(container, name, config, ticketStatus);
 % } else {
                 var viewer = new RT.LifecycleViewer();
-                viewer.initializeViewer(container, config, ticketStatus);
+                viewer.initializeViewer(container, name, config, ticketStatus);
 % }
             });
         });
diff --git a/html/Elements/LifecycleInspectorCanvas b/html/Elements/LifecycleInspectorCanvas
index 77fee55..c2ba567 100644
--- a/html/Elements/LifecycleInspectorCanvas
+++ b/html/Elements/LifecycleInspectorCanvas
@@ -1,5 +1,7 @@
 <script type="text/x-template" class="lifecycle-inspector-template" data-type="canvas">
     <div class="canvas">
+        Lifecycle: {{lifecycle.name}}<br>
+
         <button class="add-status"><&|/l&>Add Status</&></button><br>
         <ul>
           <li>Add Decoration...
diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index b6ca271..ba910ac 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -445,9 +445,9 @@ jQuery(function () {
         this.selectDecoration(line._key);
     };
 
-    Editor.prototype.initializeEditor = function (node, config, focusStatus) {
+    Editor.prototype.initializeEditor = function (node, name, config, focusStatus) {
         var self = this;
-        self.initializeViewer(node, config);
+        self.initializeViewer(node, name, config, focusStatus);
 
         self.templates = self._initializeTemplates(self.container);
         self.inspector = self.container.find('.inspector');
diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index 6738732..28884eb 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -2,7 +2,8 @@ jQuery(function () {
     var _ELEMENT_KEY_SEQ = 0;
     var defaultColors = d3.scaleOrdinal(d3.schemeCategory10);
 
-    function Lifecycle () {
+    function Lifecycle (name) {
+        this.name = name;
         this.type = 'ticket';
         this.statuses = [];
         this.defaults = {};
diff --git a/static/js/lifecycleui-viewer.js b/static/js/lifecycleui-viewer.js
index 8134b94..77a7700 100644
--- a/static/js/lifecycleui-viewer.js
+++ b/static/js/lifecycleui-viewer.js
@@ -360,7 +360,7 @@ jQuery(function () {
         return false;
     };
 
-    Viewer.prototype.initializeViewer = function (node, config, focusStatus) {
+    Viewer.prototype.initializeViewer = function (node, name, config, focusStatus) {
         var self = this;
 
         self.container = jQuery(node);
@@ -375,7 +375,7 @@ jQuery(function () {
         self._xScaleZero = self.createScale(self.width, 0);
         self._yScaleZero = self.createScale(self.height, 0);
 
-        self.lifecycle = new RT.Lifecycle();
+        self.lifecycle = new RT.Lifecycle(name);
         self.lifecycle.initializeFromConfig(config);
 
         self.addZoomBehavior();

commit f0b6d41190530a15d2dfc400235b69f3575942eb
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 18:36:26 2017 +0000

    Implement lifecycle defaults

diff --git a/html/Elements/LifecycleInspectorCanvas b/html/Elements/LifecycleInspectorCanvas
index c2ba567..75db9bd 100644
--- a/html/Elements/LifecycleInspectorCanvas
+++ b/html/Elements/LifecycleInspectorCanvas
@@ -1,6 +1,50 @@
 <script type="text/x-template" class="lifecycle-inspector-template" data-type="canvas">
     <div class="canvas">
         Lifecycle: {{lifecycle.name}}<br>
+        On Create: <select name="on_create">
+                     {{#select lifecycle.defaults.on_create}}
+                       <option value=""><&|/l&>(no value)</&></option>
+                       {{#each lifecycle.statuses}}
+                         <option value="{{this}}">{{this}}</option>
+                       {{/each}}
+                     {{/select}}
+                   </select><br>
+
+        Approved: <select name="approved">
+                     {{#select lifecycle.defaults.approved}}
+                       <option value=""><&|/l&>(no value)</&></option>
+                       {{#each lifecycle.statuses}}
+                         <option value="{{this}}">{{this}}</option>
+                       {{/each}}
+                     {{/select}}
+                   </select><br>
+
+        Denied: <select name="denied">
+                     {{#select lifecycle.defaults.denied}}
+                       <option value=""><&|/l&>(no value)</&></option>
+                       {{#each lifecycle.statuses}}
+                         <option value="{{this}}">{{this}}</option>
+                       {{/each}}
+                     {{/select}}
+                   </select><br>
+
+        Reminder on Open: <select name="reminder_on_open">
+                     {{#select lifecycle.defaults.reminder_on_open}}
+                       <option value=""><&|/l&>(no value)</&></option>
+                       {{#each lifecycle.statuses}}
+                         <option value="{{this}}">{{this}}</option>
+                       {{/each}}
+                     {{/select}}
+                   </select><br>
+
+        Reminder on Resolve: <select name="reminder_on_resolve">
+                     {{#select lifecycle.defaults.reminder_on_resolve}}
+                       <option value=""><&|/l&>(no value)</&></option>
+                       {{#each lifecycle.statuses}}
+                         <option value="{{this}}">{{this}}</option>
+                       {{/each}}
+                     {{/select}}
+                   </select><br>
 
         <button class="add-status"><&|/l&>Add Status</&></button><br>
         <ul>
diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index ba910ac..394681b 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -93,6 +93,9 @@ jQuery(function () {
                 var action = lifecycle.itemForKey(action.data('key'));
                 lifecycle.updateItem(action, field, value);
             }
+            else if (inspector.find('.canvas').length) {
+                lifecycle.update(field, value);
+            }
             else {
                 lifecycle.updateItem(self.inspectorNode, field, value);
             }
diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index 28884eb..d8e143d 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -608,6 +608,15 @@ jQuery(function () {
         return item;
     };
 
+    Lifecycle.prototype.update = function (field, value) {
+        if (field == 'on_create' || field == 'approved' || field == 'denied' || field == 'reminder_on_open' || field == 'reminder_on_resolve') {
+            this.defaults[field] = value;
+        }
+        else {
+            console.error("Unhandled field in Lifecycle.update: " + field);
+        }
+    };
+
     RT.Lifecycle = Lifecycle;
 });
 

commit d16c84cc6da514601c5d05fa6d31c2957f1ef712
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 18:39:30 2017 +0000

    Set type sooner

diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index d8e143d..a8a69b9 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -231,6 +231,7 @@ jQuery(function () {
     Lifecycle.prototype.exportAsConfiguration = function () {
         var self = this;
         var config = {
+            type: self.type,
             initial: [],
             active: [],
             inactive: [],
@@ -244,8 +245,6 @@ jQuery(function () {
             transitionExtra: {}
         };
 
-        config.type = self.type;
-
         var transitions = { "": [] };
 
         jQuery.each(self.statuses, function (i, statusName) {

commit 3b2731f54631bd7b97a2c83487df7170fa76a1ca
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 18:46:04 2017 +0000

    Allow selecting whether tickets should display lifecycle

diff --git a/html/Callbacks/RT-Extension-LifecycleUI/Ticket/Elements/ShowSummary/AfterReminders b/html/Callbacks/RT-Extension-LifecycleUI/Ticket/Elements/ShowSummary/AfterReminders
index d14c252..1d3c1e7 100644
--- a/html/Callbacks/RT-Extension-LifecycleUI/Ticket/Elements/ShowSummary/AfterReminders
+++ b/html/Callbacks/RT-Extension-LifecycleUI/Ticket/Elements/ShowSummary/AfterReminders
@@ -2,7 +2,8 @@
 $Ticket
 </%ARGS>
 <%INIT>
-return if 0; # XXX check to see if lifecycle should show on ticket
+my $config = $Ticket->LifecycleObj->{data}{ticket_display} || 'hidden';
+return unless $config eq 'readonly' || $config eq 'interactive';
 </%INIT>
 <&| /Widgets/TitleBox,
     title => loc("Lifecycle"),
diff --git a/html/Elements/LifecycleInspectorCanvas b/html/Elements/LifecycleInspectorCanvas
index 75db9bd..2f1a0e6 100644
--- a/html/Elements/LifecycleInspectorCanvas
+++ b/html/Elements/LifecycleInspectorCanvas
@@ -1,6 +1,15 @@
 <script type="text/x-template" class="lifecycle-inspector-template" data-type="canvas">
     <div class="canvas">
         Lifecycle: {{lifecycle.name}}<br>
+
+        Lifecycle on Tickets: <select name="ticket_display">
+                     {{#select lifecycle.ticket_display}}
+                       <option value="hidden"><&|/l&>hidden</&>
+                       <option value="readonly"><&|/l&>read-only</&>
+                       <option value="interactive"><&|/l&>interactive</&>
+                     {{/select}}
+                   </select><br><br>
+
         On Create: <select name="on_create">
                      {{#select lifecycle.defaults.on_create}}
                        <option value=""><&|/l&>(no value)</&></option>
diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index a8a69b9..74783d1 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -35,6 +35,10 @@ jQuery(function () {
             self.type = config.type;
         }
 
+        if (config.ticket_display) {
+            self.ticket_display = config.ticket_display;
+        }
+
         jQuery.each(['initial', 'active', 'inactive'], function (i, type) {
             if (config[type]) {
                 self.statuses = self.statuses.concat(config[type]);
@@ -240,6 +244,7 @@ jQuery(function () {
             rights: {},
             transitions: self.transitions,
 
+            ticket_display: self.ticket_display,
             decorations: {},
             statusExtra: {},
             transitionExtra: {}
@@ -611,6 +616,9 @@ jQuery(function () {
         if (field == 'on_create' || field == 'approved' || field == 'denied' || field == 'reminder_on_open' || field == 'reminder_on_resolve') {
             this.defaults[field] = value;
         }
+        else if (field == 'ticket_display') {
+            this[field] = value;
+        }
         else {
             console.error("Unhandled field in Lifecycle.update: " + field);
         }

commit bdd22b1d265f1ac84361c4f89f1dc02b643b778e
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 18:50:00 2017 +0000

    Only display Lifecycle on Ticket for tickets

diff --git a/html/Elements/LifecycleInspectorCanvas b/html/Elements/LifecycleInspectorCanvas
index 2f1a0e6..59df670 100644
--- a/html/Elements/LifecycleInspectorCanvas
+++ b/html/Elements/LifecycleInspectorCanvas
@@ -1,14 +1,18 @@
 <script type="text/x-template" class="lifecycle-inspector-template" data-type="canvas">
     <div class="canvas">
-        Lifecycle: {{lifecycle.name}}<br>
+        Name: {{lifecycle.name}}<br>
+        Type: {{lifecycle.type}}<br>
 
+        {{#if lifecycle.is_ticket}}
         Lifecycle on Tickets: <select name="ticket_display">
                      {{#select lifecycle.ticket_display}}
                        <option value="hidden"><&|/l&>hidden</&>
                        <option value="readonly"><&|/l&>read-only</&>
                        <option value="interactive"><&|/l&>interactive</&>
                      {{/select}}
-                   </select><br><br>
+                   </select><br>
+        {{/if}}
+        <br>
 
         On Create: <select name="on_create">
                      {{#select lifecycle.defaults.on_create}}
diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index 74783d1..5aaa4e9 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -5,6 +5,7 @@ jQuery(function () {
     function Lifecycle (name) {
         this.name = name;
         this.type = 'ticket';
+        this.is_ticket = true;
         this.statuses = [];
         this.defaults = {};
         this.transitions = [];
@@ -33,6 +34,7 @@ jQuery(function () {
 
         if (config.type) {
             self.type = config.type;
+            self.is_ticket = self.type == 'ticket';
         }
 
         if (config.ticket_display) {

commit 75ee81a2f4e2977eab17952aa4c9184512af5156
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 19:00:40 2017 +0000

    Improve lifecycle index

diff --git a/html/Admin/Lifecycles/index.html b/html/Admin/Lifecycles/index.html
index 6c5f84f..39f6a0c 100644
--- a/html/Admin/Lifecycles/index.html
+++ b/html/Admin/Lifecycles/index.html
@@ -1,19 +1,25 @@
 <& /Admin/Elements/Header, Title => loc("Admin Lifecycles") &>
 <& /Elements/Tabs &>
 
-% for my $type (@types) {
-% my @lifecycles = @{ $lifecycles{$type} };
-<h2><&|/l, $type&>"[_1]" lifecycles</&></h2>
-% if (@lifecycles) {
-<ul>
+<h1><&|/l&>Lifecycles</&></h1>
+
+<table cellspacing="0" class="collection collection-as-table">
+<tr class="collection-as-table">
+  <th class="collection-as-table"><&|/l&>Name</&></th>
+  <th class="collection-as-table"><&|/l&>Type</&></th>
+  <th class="collection-as-table"><&|/l&>Display</&></th>
+</tr>
+% my $i = 0;
 % for my $lifecycle (@lifecycles) {
-<li><a href="<% RT->Config->Get('WebURL') %>Admin/Lifecycles/Modify.html?Type=<% $type |u %>&Name=<% $lifecycle |u %>"><% $lifecycle %></a></li>
-% }
-</ul>
-% } else {
-<p><&|/l&>No lifecycles.</&></p>
-% }
+% ++$i;
+<tr class="<% $i % 2 ? 'oddline' : 'evenline' %>">
+<td class="collection-as-table"><a href="<% RT->Config->Get('WebURL') %>Admin/Lifecycles/Modify.html?Type=<% $lifecycle->Type |u %>&Name=<% $lifecycle->Name |u %>"><% $lifecycle->Name %></a></td>
+<td class="collection-as-table"><% loc($lifecycle->Type) %></td>
+% my $display = $lifecycle->Type eq 'ticket' ? ($lifecycle->{data}{ticket_display} || 'hidden') : '';
+<td class="collection-as-table"><% loc($display) %></td>
+</tr>
 % }
+</table>
 <%INIT>
 my @types = List::MoreUtils::uniq(
     'ticket',
@@ -21,11 +27,12 @@ my @types = List::MoreUtils::uniq(
     sort keys %RT::Lifecycle::LIFECYCLES_TYPES,
 );
 
-my %lifecycles;
+my @lifecycles;
 
 for my $type (@types) {
-    @{ $lifecycles{$type} } = sort { loc($a) cmp loc($b) }
-                              grep { $_ ne 'approvals' }
-                              RT::Lifecycle->ListAll($type);
+    push @lifecycles, map { RT::Lifecycle->Load(Name => $_, Type => $type) }
+                      sort { loc($a) cmp loc($b) }
+                      grep { $_ ne 'approvals' }
+                      RT::Lifecycle->ListAll($type);
 }
 </%INIT>

commit 2a3120e2162acbecfe4b8b2e44cff96cb08b4457
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 19:09:50 2017 +0000

    Improve identifying current object in inspector

diff --git a/html/Elements/LifecycleInspectorAction b/html/Elements/LifecycleInspectorAction
index a49ad8a..9774a49 100644
--- a/html/Elements/LifecycleInspectorAction
+++ b/html/Elements/LifecycleInspectorAction
@@ -1,6 +1,6 @@
 <script type="text/x-template" class="lifecycle-inspector-template" data-type="action">
     <li class="action" data-key="{{action._key}}">
-        Label: <input type="text" name="label" value="{{action.label}}"></input><br>
+        Label: <input type="text" name="label" value="{{action.label}}"></input><br><br>
         Update: <select name="update">
                   {{#select action.update}}
                   <option value=""><&|/l&>quick</&></option>
diff --git a/html/Elements/LifecycleInspectorCanvas b/html/Elements/LifecycleInspectorCanvas
index 59df670..20f5bb6 100644
--- a/html/Elements/LifecycleInspectorCanvas
+++ b/html/Elements/LifecycleInspectorCanvas
@@ -1,6 +1,6 @@
 <script type="text/x-template" class="lifecycle-inspector-template" data-type="canvas">
     <div class="canvas">
-        Name: {{lifecycle.name}}<br>
+        Lifecycle: {{lifecycle.name}}<br><br>
         Type: {{lifecycle.type}}<br>
 
         {{#if lifecycle.is_ticket}}
diff --git a/html/Elements/LifecycleInspectorCircle b/html/Elements/LifecycleInspectorCircle
index 43ddca8..b435af1 100644
--- a/html/Elements/LifecycleInspectorCircle
+++ b/html/Elements/LifecycleInspectorCircle
@@ -1,6 +1,6 @@
 <script type="text/x-template" class="lifecycle-inspector-template" data-type="circle">
     <div class="circle">
-       Label: <input type="text" name="label" value="{{circle.label}}" /><br>
+       Label: <input type="text" name="label" value="{{circle.label}}" /><br><br>
        Border: <input type="checkbox" name="renderStroke" {{#if circle.renderStroke}}checked=checked{{/if}} data-show-hide=".color-control[data-field=stroke], .stroke-style"> <span class="color-control" data-field="stroke"><span class="current-color" title="{{circle.stroke}}" style="background-color: {{circle.stroke}}"> </span> <button class="change-color"><&|/l&>Change</&></button></span><br>
        <div class="stroke-style">Style: <select name="strokeStyle">
                  {{#select circle.strokeStyle}}
diff --git a/html/Elements/LifecycleInspectorLine b/html/Elements/LifecycleInspectorLine
index 26ab774..076cbc9 100644
--- a/html/Elements/LifecycleInspectorLine
+++ b/html/Elements/LifecycleInspectorLine
@@ -1,6 +1,6 @@
 <script type="text/x-template" class="lifecycle-inspector-template" data-type="line">
     <div class="line">
-       Label: <input type="text" name="label" value="{{line.label}}" /><br>
+       Label: <input type="text" name="label" value="{{line.label}}" /><br><br>
        Start Marker: <select name="startMarker">
                  {{#select line.startMarker}}
                  <option value="none"><&|/l&>none</&></option>
diff --git a/html/Elements/LifecycleInspectorPolygon b/html/Elements/LifecycleInspectorPolygon
index 0f57947..bcc99dc 100644
--- a/html/Elements/LifecycleInspectorPolygon
+++ b/html/Elements/LifecycleInspectorPolygon
@@ -1,6 +1,6 @@
 <script type="text/x-template" class="lifecycle-inspector-template" data-type="polygon">
     <div class="polygon">
-       Label: <input type="text" name="label" value="{{polygon.label}}" /><br>
+       Label: <input type="text" name="label" value="{{polygon.label}}" /><br><br>
        Border: <input type="checkbox" name="renderStroke" {{#if polygon.renderStroke}}checked=checked{{/if}} data-show-hide=".color-control[data-field=stroke], .stroke-style"> <span class="color-control" data-field="stroke"><span class="current-color" title="{{polygon.stroke}}" style="background-color: {{polygon.stroke}}"> </span> <button class="change-color"><&|/l&>Change</&></button></span><br>
        <div class="stroke-style">Style: <select name="strokeStyle">
                  {{#select polygon.strokeStyle}}
diff --git a/html/Elements/LifecycleInspectorStatus b/html/Elements/LifecycleInspectorStatus
index 8cbaceb..035fa6c 100644
--- a/html/Elements/LifecycleInspectorStatus
+++ b/html/Elements/LifecycleInspectorStatus
@@ -1,6 +1,6 @@
 <script type="text/x-template" class="lifecycle-inspector-template" data-type="status">
     <div class="status">
-        Name: <input type="text" name="name" maxlength=25 value="{{status.name}}"><br>
+        Status: <input type="text" name="name" maxlength=25 value="{{status.name}}"><br><br>
         Type: <select name="type">
                   {{#select status.type}}
                   <option value="initial"><&|/l&>initial</&></option>
diff --git a/html/Elements/LifecycleInspectorText b/html/Elements/LifecycleInspectorText
index 914ecb1..7c7baae 100644
--- a/html/Elements/LifecycleInspectorText
+++ b/html/Elements/LifecycleInspectorText
@@ -1,6 +1,6 @@
 <script type="text/x-template" class="lifecycle-inspector-template" data-type="text">
     <div class="text">
-        Body: <input type="text" name="text" value="{{text.text}}" /><br>
+        Text: <input type="text" name="text" value="{{text.text}}" /><br><br>
         Bold: <input type="checkbox" name="bold" {{#if text.bold}}checked=checked{{/if}}><br>
         Italic: <input type="checkbox" name="italic" {{#if text.italic}}checked=checked{{/if}}><br>
         <button class="delete"><&|/l&>Delete Text</&></button>
diff --git a/html/Elements/LifecycleInspectorTransition b/html/Elements/LifecycleInspectorTransition
index d2fac11..dfc4d58 100644
--- a/html/Elements/LifecycleInspectorTransition
+++ b/html/Elements/LifecycleInspectorTransition
@@ -1,7 +1,6 @@
 <script type="text/x-template" class="lifecycle-inspector-template" data-type="transition">
     <div class="transition">
-       From: <a href="#" class="select-status" data-name="{{transition.from}}">{{transition.from}}</a><br>
-       To: <a href="#" class="select-status" data-name="{{transition.to}}">{{transition.to}}</a><br>
+       Transition from <a href="#" class="select-status" data-name="{{transition.from}}">{{transition.from}}</a> to <a href="#" class="select-status" data-name="{{transition.to}}">{{transition.to}}</a><br><br>
        Right: <input type="text" name="right" value="{{transition.right}}"><br>
        Style: <select name="style">
                  {{#select transition.style}}

commit 8c58f9ae3782b1dfef935204180df1b910cdab66
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 19:21:44 2017 +0000

    Switch .editing to be on parent .lifecycle-ui element

diff --git a/html/Elements/LifecycleGraph b/html/Elements/LifecycleGraph
index 1ddd5f3..7ce1ba3 100644
--- a/html/Elements/LifecycleGraph
+++ b/html/Elements/LifecycleGraph
@@ -1,5 +1,5 @@
-<div class="lifecycle-ui" id="lifecycle-<% $id %>">
-    <svg<% $Editing ? ' class="editing"' : '' |n %>>
+<div class="lifecycle-ui<% $Editing ? ' editing' : '' %>" id="lifecycle-<% $id %>">
+    <svg>
         <& /Elements/LifecycleGraphExtras, %ARGS &>
         <g class="decorations"></g>
         <g class="transitions"></g>
diff --git a/static/css/lifecycleui-editor.css b/static/css/lifecycleui-editor.css
index 5e7d4b9..72320b7 100644
--- a/static/css/lifecycleui-editor.css
+++ b/static/css/lifecycleui-editor.css
@@ -1,4 +1,4 @@
-.lifecycle-ui svg.editing {
+.lifecycle-ui.editing svg {
     display: inline-block;
     float: left;
     width: 809px;
@@ -48,21 +48,21 @@
     padding: .5em;
 }
 
-.lifecycle-ui svg.editing.has-focus .decorations > * {
+.lifecycle-ui.editing svg.has-focus .decorations > * {
     opacity: .15;
 }
 
-.lifecycle-ui svg.editing.has-focus .decorations .focus,
-.lifecycle-ui svg.editing.has-focus .decorations .point-handle,
-.lifecycle-ui svg.editing.has-focus .transitions .focus {
+.lifecycle-ui.editing svg.has-focus .decorations .focus,
+.lifecycle-ui.editing svg.has-focus .decorations .point-handle,
+.lifecycle-ui.editing svg.has-focus .transitions .focus {
     opacity: 1;
 }
 
-.lifecycle-ui svg.editing.has-focus[data-focus-type=transition] .statuses .focus-from {
+.lifecycle-ui.editing svg.has-focus[data-focus-type=transition] .statuses .focus-from {
     opacity: 1;
 }
 
-.lifecycle-ui svg.editing.has-focus .decorations .text-background {
+.lifecycle-ui.editing svg.has-focus .decorations .text-background {
     stroke: none;
     fill: white;
     opacity: 1;

commit 7a8c20807bf1f323b76d7cb299b6ae213d4b6b48
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 19:25:51 2017 +0000

    Render zoom buttons on top of svg

diff --git a/html/Elements/LifecycleGraph b/html/Elements/LifecycleGraph
index 7ce1ba3..a5d7012 100644
--- a/html/Elements/LifecycleGraph
+++ b/html/Elements/LifecycleGraph
@@ -1,4 +1,9 @@
 <div class="lifecycle-ui<% $Editing ? ' editing' : '' %>" id="lifecycle-<% $id %>">
+    <div class="overlay-buttons">
+        <button class="zoom-in">+</button>
+        <button class="zoom-reset">0</button>
+        <button class="zoom-out">-</button>
+    </div>
     <svg>
         <& /Elements/LifecycleGraphExtras, %ARGS &>
         <g class="decorations"></g>
diff --git a/static/css/lifecycleui-editor.css b/static/css/lifecycleui-editor.css
index 72320b7..cfa39ec 100644
--- a/static/css/lifecycleui-editor.css
+++ b/static/css/lifecycleui-editor.css
@@ -13,6 +13,10 @@
     background-size: 25px 25px;
 }
 
+.lifecycle-ui.editing .overlay-buttons {
+    left: 700px;
+}
+
 .lifecycle-ui .inspector {
     display: inline-block;
     width: 250px;
diff --git a/static/css/lifecycleui-viewer.css b/static/css/lifecycleui-viewer.css
index 4dbc93d..d8bfc50 100644
--- a/static/css/lifecycleui-viewer.css
+++ b/static/css/lifecycleui-viewer.css
@@ -4,6 +4,12 @@
     height: 309px;
 }
 
+.lifecycle-ui .overlay-buttons {
+    position: absolute;
+    top: 1em;
+    left: 391px;
+}
+
 .lifecycle-ui .statuses circle {
     stroke: black;
     stroke-width: 2px;
@@ -92,3 +98,10 @@
     font-style: italic;
 }
 
+.lifecycle-ui {
+    position: relative;
+}
+
+.lifecycle-ui .overlay-buttons button {
+    border: none;
+}

commit 34df98b535e0fe9970921c3121bad0c3ea1bb7da
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 19:26:12 2017 +0000

    Add undo/redo buttons at top of inspector

diff --git a/html/Elements/LifecycleInspector b/html/Elements/LifecycleInspector
index 8d90b65..ff655b3 100644
--- a/html/Elements/LifecycleInspector
+++ b/html/Elements/LifecycleInspector
@@ -1,4 +1,10 @@
 <div class="inspector">
+  <div class="controls">
+    <button class="undo">Undo</button>
+    <button class="redo">Redo</button>
+  </div>
+  <div class="content">
+  </div>
 </div>
 
 <& LifecycleInspectorCanvas, %ARGS &>
diff --git a/static/css/lifecycleui-editor.css b/static/css/lifecycleui-editor.css
index cfa39ec..71cdb5a 100644
--- a/static/css/lifecycleui-editor.css
+++ b/static/css/lifecycleui-editor.css
@@ -20,11 +20,19 @@
 .lifecycle-ui .inspector {
     display: inline-block;
     width: 250px;
-    padding: 10px;
-    min-height: 480px;
+    min-height: 500px;
     border: 1px solid black;
 }
 
+.lifecycle-ui .inspector .content {
+    padding: 1em;
+}
+
+.lifecycle-ui .inspector .controls {
+    padding: 1em;
+    background: #EEE;
+}
+
 .lifecycle-ui .inspector .color-control span.current-color {
     display: inline;
     padding-left: 1em;
diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 394681b..4d54b2a 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -56,7 +56,7 @@ jQuery(function () {
         var params = { lifecycle: lifecycle };
         params[type] = node;
 
-        inspector.html(self.templates[type](params));
+        inspector.find('.content').html(self.templates[type](params));
 
         inspector.find(':checkbox[data-show-hide]').each(function () {
             var field = jQuery(this);
@@ -223,6 +223,14 @@ jQuery(function () {
             e.preventDefault();
             self.addNewLineDecoration();
         });
+
+        inspector.on('click', 'button.undo', function (e) {
+            e.preventDefault();
+        });
+
+        inspector.on('click', 'button.redo', function (e) {
+            e.preventDefault();
+        });
     };
 
     Editor.prototype.deselectAll = function (clearSelection) {

commit e6692748815d94f85e64f1eb6362b9b96701087f
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 19:33:04 2017 +0000

    Add a g.transform frame

diff --git a/html/Elements/LifecycleGraph b/html/Elements/LifecycleGraph
index a5d7012..7bb5518 100644
--- a/html/Elements/LifecycleGraph
+++ b/html/Elements/LifecycleGraph
@@ -6,9 +6,11 @@
     </div>
     <svg>
         <& /Elements/LifecycleGraphExtras, %ARGS &>
-        <g class="decorations"></g>
-        <g class="transitions"></g>
-        <g class="statuses"></g>
+        <g class="transform">
+          <g class="decorations"></g>
+          <g class="transitions"></g>
+          <g class="statuses"></g>
+        </g>
     </svg>
 % if ($Editing) {
     <& /Elements/LifecycleInspector &>
diff --git a/static/js/lifecycleui-viewer.js b/static/js/lifecycleui-viewer.js
index 77a7700..7d6e458 100644
--- a/static/js/lifecycleui-viewer.js
+++ b/static/js/lifecycleui-viewer.js
@@ -32,14 +32,14 @@ jQuery(function () {
     };
 
     Viewer.prototype.didZoom = function () {
-        this.svg.selectAll("g").attr("transform", d3.event.transform);
+        this.transformContainer.attr("transform", d3.event.transform);
     };
 
     Viewer.prototype.resetZoom = function () {
-        this.svg.selectAll("g")
+        this.transformContainer
                 .transition()
                 .duration(750)
-                .call(self._zoom.transform, d3.zoomIdentity);
+                .call(this._zoom.transform, d3.zoomIdentity);
     };
 
     Viewer.prototype.didEnterStatusNodes = function (statuses) { };
@@ -287,8 +287,7 @@ jQuery(function () {
     Viewer.prototype.centerOnItem = function (item) {
         var x = this.xScale(item.x);
         var y = this.yScale(item.y);
-        this.svg.selectAll("g")
-                .call(this._zoom.translateTo, x, y);
+        this.transformContainer.call(this._zoom.translateTo, x, y);
     };
 
     Viewer.prototype.defocus = function () {
@@ -366,6 +365,7 @@ jQuery(function () {
         self.container = jQuery(node);
         self.svg       = d3.select(node).select('svg');
 
+        self.transformContainer  = self.svg.select('g.transform');
         self.transitionContainer = self.svg.select('g.transitions');
         self.statusContainer     = self.svg.select('g.statuses');
         self.decorationContainer = self.svg.select('g.decorations');

commit 0549d86bee25323ce5cf155051539a38bfce9a53
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 19:51:03 2017 +0000

    Improve automatic/default zoom behavior

diff --git a/static/js/lifecycleui-viewer.js b/static/js/lifecycleui-viewer.js
index 7d6e458..7b5fe8a 100644
--- a/static/js/lifecycleui-viewer.js
+++ b/static/js/lifecycleui-viewer.js
@@ -35,11 +35,15 @@ jQuery(function () {
         this.transformContainer.attr("transform", d3.event.transform);
     };
 
-    Viewer.prototype.resetZoom = function () {
-        this.transformContainer
-                .transition()
-                .duration(750)
-                .call(this._zoom.transform, d3.zoomIdentity);
+    Viewer.prototype.resetZoom = function (animated) {
+        if (animated) {
+            this.svg.transition()
+                    .duration(750)
+                    .call(this._zoom.transform, this._zoomIdentity);
+        }
+        else {
+            this.svg.call(this._zoom.transform, this._zoomIdentity);
+        }
     };
 
     Viewer.prototype.didEnterStatusNodes = function (statuses) { };
@@ -284,10 +288,12 @@ jQuery(function () {
         this.renderDecorations(initial);
     };
 
-    Viewer.prototype.centerOnItem = function (item) {
-        var x = this.xScale(item.x);
-        var y = this.yScale(item.y);
-        this.transformContainer.call(this._zoom.translateTo, x, y);
+    Viewer.prototype.centerOnItem = function (item, animated) {
+        var rect = this.svg.node().getBoundingClientRect();
+        var x = rect.width/2 - this.xScale(item.x);
+        var y = rect.height/2 - this.yScale(item.y);
+        this._zoomIdentity = d3.zoomIdentity.translate(x, y);
+        this.resetZoom(animated);
     };
 
     Viewer.prototype.defocus = function () {
@@ -302,7 +308,7 @@ jQuery(function () {
                 .attr('data-focus-type', d._type);
     };
 
-    Viewer.prototype.focusOnStatus = function (statusName, center) {
+    Viewer.prototype.focusOnStatus = function (statusName, center, animated) {
         if (!statusName) {
             return;
         }
@@ -311,7 +317,7 @@ jQuery(function () {
         this.focusItem(meta);
 
         if (center) {
-            this.centerOnItem(meta)
+            this.centerOnItem(meta, animated)
         }
     };
 
@@ -374,13 +380,14 @@ jQuery(function () {
         self._yScale = self.createScale(self.height, self.padding);
         self._xScaleZero = self.createScale(self.width, 0);
         self._yScaleZero = self.createScale(self.height, 0);
+        self._zoomIdentity = d3.zoomIdentity;
 
         self.lifecycle = new RT.Lifecycle(name);
         self.lifecycle.initializeFromConfig(config);
 
         self.addZoomBehavior();
 
-        self.focusOnStatus(focusStatus, true);
+        self.focusOnStatus(focusStatus, true, false);
 
         self.renderDisplay(true);
     };

commit 37f0ab466519471d2cc72027d52140080fa7abcf
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 19:55:15 2017 +0000

    Implement zoom buttons

diff --git a/static/js/lifecycleui-viewer.js b/static/js/lifecycleui-viewer.js
index 7b5fe8a..167823b 100644
--- a/static/js/lifecycleui-viewer.js
+++ b/static/js/lifecycleui-viewer.js
@@ -35,6 +35,17 @@ jQuery(function () {
         this.transformContainer.attr("transform", d3.event.transform);
     };
 
+    Viewer.prototype.zoomScale = function (scaleBy, animated) {
+        if (animated) {
+            this.svg.transition()
+                    .duration(350)
+                    .call(this._zoom.scaleBy, scaleBy);
+        }
+        else {
+            this.svg.call(this._zoom.scaleBy, scaleBy);
+        }
+    }
+
     Viewer.prototype.resetZoom = function (animated) {
         if (animated) {
             this.svg.transition()
@@ -390,6 +401,21 @@ jQuery(function () {
         self.focusOnStatus(focusStatus, true, false);
 
         self.renderDisplay(true);
+
+        self.container.on('click', 'button.zoom-in', function (e) {
+            e.preventDefault();
+            self.zoomScale(1.25, true);
+        });
+
+        self.container.on('click', 'button.zoom-out', function (e) {
+            e.preventDefault();
+            self.zoomScale(.75, true);
+        });
+
+        self.container.on('click', 'button.zoom-reset', function (e) {
+            e.preventDefault();
+            self.resetZoom(true);
+        });
     };
 
     RT.LifecycleViewer = Viewer;

commit 47bbc7c2c1cf772c95ea1fef4d6329014b1205ab
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 20:36:56 2017 +0000

    Handle clicking undo with nothing to undo

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 4d54b2a..36f529a 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -226,6 +226,29 @@ jQuery(function () {
 
         inspector.on('click', 'button.undo', function (e) {
             e.preventDefault();
+            var payload = self.lifecycle.undo();
+            if (!payload) {
+                return;
+            }
+
+            if (payload.inspectorKey) {
+                var node = self.lifecycle.itemForKey(payload.inspectorKey);
+                self.setInspectorContent(node);
+            }
+            else {
+                self.setInspectorContent(null);
+            }
+
+            if (payload.focusKey) {
+                var node = self.lifecycle.itemForKey(payload.focusKey);
+                self.focusItem(node);
+            }
+            else {
+                self.defocus();
+            }
+
+            self.renderDisplay();
+
         });
 
         inspector.on('click', 'button.redo', function (e) {
@@ -476,6 +499,17 @@ jQuery(function () {
         });
 
         self.svg.on('click', function () { self.deselectAll(true) });
+
+        self.lifecycle.saveUndoCallback = function () {
+            var payload = {};
+            if (self.inspectorNode) {
+                payload.inspectorKey = self.inspectorNode._key;
+            }
+            if (self._focusItem) {
+                payload.focusKey = self._focusItem._key;
+            }
+            return payload;
+        };
     };
 
     RT.LifecycleEditor = Editor;
diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index 5aaa4e9..ebefc6f 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -11,6 +11,7 @@ jQuery(function () {
         this.transitions = [];
         this.decorations = {};
 
+        this._undoStack = [];
         this._keyMap = {};
         this._statusMeta = {};
 
@@ -355,6 +356,8 @@ jQuery(function () {
     Lifecycle.prototype.deleteStatus = function (key) {
         var self = this;
 
+        self._saveUndoEntry();
+
         var statusName = self.statusNameForKey(key);
         if (!statusName) {
             console.error("no status for key '" + key + "'; did you accidentally pass status name?");
@@ -385,6 +388,8 @@ jQuery(function () {
     };
 
     Lifecycle.prototype.addTransition = function (fromStatus, toStatus) {
+        this._saveUndoEntry();
+
         var transition = {
             _key    : _ELEMENT_KEY_SEQ++,
             _type   : 'transition',
@@ -439,6 +444,8 @@ jQuery(function () {
     };
 
     Lifecycle.prototype.deleteTransition = function (key) {
+        this._saveUndoEntry();
+
         this.transitions = jQuery.grep(this.transitions, function (transition) {
             if (transition._key == key) {
                 return false;
@@ -449,6 +456,8 @@ jQuery(function () {
     };
 
     Lifecycle.prototype.deleteDecoration = function (type, key) {
+        this._saveUndoEntry();
+
         this.decorations[type] = jQuery.grep(this.decorations[type], function (decoration) {
             if (decoration._key == key) {
                 return false;
@@ -481,6 +490,8 @@ jQuery(function () {
     };
 
     Lifecycle.prototype.deleteActionForTransition = function (transition, key) {
+        this._saveUndoEntry();
+
         transition.actions = jQuery.grep(transition.actions, function (action) {
             if (action._key == key) {
                 return false;
@@ -491,6 +502,8 @@ jQuery(function () {
     };
 
     Lifecycle.prototype.updateItem = function (item, field, newValue) {
+        this._saveUndoEntry();
+
         var oldValue = item[field];
 
         item[field] = newValue;
@@ -501,6 +514,8 @@ jQuery(function () {
     };
 
     Lifecycle.prototype.createActionForTransition = function (transition) {
+        this._saveUndoEntry();
+
         var action = {
             _type : 'action',
             _key  : _ELEMENT_KEY_SEQ++,
@@ -522,6 +537,8 @@ jQuery(function () {
     };
 
     Lifecycle.prototype.createStatus = function () {
+        this._saveUndoEntry();
+
         var name;
         var i = 0;
         while (1) {
@@ -549,6 +566,8 @@ jQuery(function () {
     };
 
     Lifecycle.prototype.createTextDecoration = function () {
+        this._saveUndoEntry();
+
         var item = {
             _key: _ELEMENT_KEY_SEQ++,
             _type: 'text',
@@ -562,6 +581,8 @@ jQuery(function () {
     };
 
     Lifecycle.prototype.createPolygonDecoration = function (type) {
+        this._saveUndoEntry();
+
         var item = {
             _key: _ELEMENT_KEY_SEQ++,
             _type: 'polygon',
@@ -581,6 +602,8 @@ jQuery(function () {
     };
 
     Lifecycle.prototype.createCircleDecoration = function () {
+        this._saveUndoEntry();
+
         var item = {
             _key: _ELEMENT_KEY_SEQ++,
             _type: 'circle',
@@ -600,6 +623,8 @@ jQuery(function () {
     };
 
     Lifecycle.prototype.createLineDecoration = function () {
+        this._saveUndoEntry();
+
         var item = {
             _key: _ELEMENT_KEY_SEQ++,
             _type: 'line',
@@ -615,6 +640,8 @@ jQuery(function () {
     };
 
     Lifecycle.prototype.update = function (field, value) {
+        this._saveUndoEntry();
+
         if (field == 'on_create' || field == 'approved' || field == 'denied' || field == 'reminder_on_open' || field == 'reminder_on_resolve') {
             this.defaults[field] = value;
         }
@@ -626,6 +653,39 @@ jQuery(function () {
         }
     };
 
+    Lifecycle.prototype._saveUndoEntry = function () {
+        var undoStack = this._undoStack;
+        delete this._undoStack;
+        var entry = jQuery.extend(true, {}, this);
+        var extra = {};
+        if (this.saveUndoCallback) {
+            extra = this.saveUndoCallback();
+        }
+        undoStack.push([entry, extra]);
+        this._undoStack = undoStack;
+    };
+
+    Lifecycle.prototype.undo = function () {
+        var undoStack = this._undoStack;
+        if (undoStack.length == 0) {
+            return null;
+        }
+
+        delete this._undoStack;
+        var payload = undoStack.pop();
+        var entry = payload[0];
+
+        for (var key in entry) {
+            if (entry.hasOwnProperty(key)) {
+                this[key] = entry[key];
+            }
+        }
+
+        this._undoStack = undoStack;
+
+        return payload[1];
+    };
+
     RT.Lifecycle = Lifecycle;
 });
 

commit b93f3b43573cfb1b7b7801a996b4aee19e322bb6
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 20:40:56 2017 +0000

    Save undo entry for moving items
    
    This way we don't save one for every pixel of movement

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 36f529a..4e30840 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -391,6 +391,7 @@ jQuery(function () {
                      .classed("point-handle", true)
                      .call(d3.drag()
                          .subject(function (d) { return { x: d.xScale(d.x), y : d.yScale(d.y) } })
+                         .on("start", function (d) { self.lifecycle.beginDragging() })
                          .on("drag", function (d) { self.didDragPointHandle(d) })
                      )
               .merge(handles)
@@ -431,6 +432,7 @@ jQuery(function () {
         var self = this;
         return d3.drag()
                  .subject(function (d) { return { x: self.xScale(d.x), y : self.yScale(d.y) } })
+                 .on("start", function (d) { self.lifecycle.beginDragging() })
                  .on("drag", function (d) { self.didDragItem(d, this) });
     };
 
diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index ebefc6f..c02cb2c 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -525,6 +525,10 @@ jQuery(function () {
         return action;
     };
 
+    Lifecycle.prototype.beginDragging = function () {
+        this._saveUndoEntry();
+    };
+
     Lifecycle.prototype.moveItem = function (item, x, y) {
         item.x = x;
         item.y = y;

commit 6bf469d8ed93f0b4c3727df8e08b5dcad484ad63
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Sep 6 20:45:49 2017 +0000

    Avoid saving dozens of undo entries in color picker

diff --git a/static/js/lifecycleui-editor.js b/static/js/lifecycleui-editor.js
index 4e30840..7fba169 100644
--- a/static/js/lifecycleui-editor.js
+++ b/static/js/lifecycleui-editor.js
@@ -115,11 +115,16 @@ jQuery(function () {
                     return;
                 }
                 container.find('.current-color').val(newColor);
-                lifecycle.updateItem(self.inspectorNode, field, newColor);
+                lifecycle.updateItem(self.inspectorNode, field, newColor, true);
                 self.renderDisplay();
             });
             farb.setColor(self.inspectorNode[field]);
 
+            // see farbtastic's implementation
+            jQuery('*', picker).mousedown(function () {
+                self.lifecycle.beginChangingColor();
+            });
+
             var input = jQuery('<input class="current-color" size=8 maxlength=7>');
             container.find('.current-color').replaceWith(input);
             input.on('input', function () {
diff --git a/static/js/lifecycleui-model.js b/static/js/lifecycleui-model.js
index c02cb2c..b508d18 100644
--- a/static/js/lifecycleui-model.js
+++ b/static/js/lifecycleui-model.js
@@ -501,8 +501,10 @@ jQuery(function () {
         delete this._keyMap[key];
     };
 
-    Lifecycle.prototype.updateItem = function (item, field, newValue) {
-        this._saveUndoEntry();
+    Lifecycle.prototype.updateItem = function (item, field, newValue, skipUndo) {
+        if (!skipUndo) {
+            this._saveUndoEntry();
+        }
 
         var oldValue = item[field];
 
@@ -529,6 +531,10 @@ jQuery(function () {
         this._saveUndoEntry();
     };
 
+    Lifecycle.prototype.beginChangingColor = function () {
+        this._saveUndoEntry();
+    };
+
     Lifecycle.prototype.moveItem = function (item, x, y) {
         item.x = x;
         item.y = y;

-----------------------------------------------------------------------


More information about the Bps-public-commit mailing list