Closed Bug 1217344 Opened 9 years ago Closed 2 years ago

SVG and jQuery .draggable()

Categories

(Core :: SVG, defect)

defect

Tracking

()

RESOLVED INCOMPLETE
Tracking Status
platform-rel --- +

People

(Reporter: contact, Unassigned)

Details

(Whiteboard: [platform-rel-jQuery])

User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) AppleWebKit/601.2.4 (KHTML, like Gecko) Version/9.0.1 Safari/601.2.4

Steps to reproduce:

We’ve created  some SVG paths, with cursors on it, and use jQuery .draggable() to move it. We’re using AngularJS for the front end, and all the JS is in the controller of the page.


Actual results:

When we start to drag one of the cursors, it moves randomly, and do not follow the path. Works well on others explorers. jQuery : v1.11.4, AngularJS : v1.4, Firefox v41.0.2 on MacOS, same problem on Windows.
See the bug here : http://d.pr/i/HbHM


Expected results:

The cursor should follow the path, like that : http://d.pr/i/15lS3 (on Safari, Chrome, Opera)
Severity: normal → blocker
Priority: -- → P1
We're going to need a minimal testcase rather than a screenshot. I imagine you're using getCTM, getScreenCTM or getTransformToElement incorrectly though.
Flags: needinfo?(contact)
Thank you for your feedback.
We don't use fonctions getCTM, getScreenCTM or getTransformToElement.

Please find the code below :

### CLASSES ###


class Curve

  constructor: (data = {}, xml = false) ->

    this.init()

    if !_.isEmpty(data)
      @canvasId = data.curve_id+"SVG" if data.curve_id
      @curveId = data.curve_id if data.curve_id
      @curveOId = data.curve_id+"O" if data.curve_id
      @curveBId = data.curve_id+"B" if data.curve_id
      @curveData = data.curve_data if data.curve_data
      @cursorAImg = data.cursor_a_img if data.cursor_a_img
      @cursorBImg = data.cursor_b_img if data.cursor_b_img


    #TODO: make dynamic size
    # @width = @canvas.attr().width
    # @height = @canvas.attr().height

    @width = 425
    @height = 290

    @curveOMask = new CurveMask(this, {mask_id: data.curve_id+"OMask"})

    @curveBMask = new CurveMask(this, {mask_id: data.curve_id+"BMask"})

    @cursorA = new CurveCursor(this,
      cursor_id: data.curve_id+"CursorA"
      curve_id: @curveId
      if @cursorAImg
        cursor_img: @cursorAImg
    )

    @cursorA.setValue(50)

    @cursorB = new CurveCursor(this,
      cursor_id: data.curve_id+"CursorB"
      curve_id: @curveId
      if @cursorBImg
        cursor_img: @cursorBImg
    )

    @cursorB.setValue(150)

        Curve.curveYAtX = (curve, x) ->

      #in case we just want to pass the curve DOM ID instead of the whole curve object (must not contain a # (hash))
      if (typeof curve == "string")
        DomId = curve
        curve = {}
        curve.curveId = DomId.replace("#","")


      if(curve == undefined)
        console.error "Curve#constraint: curve is undefined"
        return

      if(x == undefined)
        console.error "Curve#constraint: x is undefined"
        return

      pathLength = document.getElementById(curve.curveId).getTotalLength()
      beginning = 0

      end = pathLength

      target = undefined

      while(true)

        target = Math.floor((beginning + end) / 2);
        pos = document.getElementById(curve.curveId).getPointAtLength(target)

        if ((target == end || target == beginning) && pos.x != x)
          break
        if (pos.x > x)
          end = target
        else if (pos.x < x)
          beginning = target
        else
          break

      return pos.y


class CurveCursor


  constructor: (parent, data = {}) ->

    this.init()

    if !_.isEmpty(data)

      @value = data.value if data.value
      @label = data.label if data.label
      @color = data.color if data.color

      @min = data.min if data.min
      @max = data.max if data.max
      @cursorId = data.cursor_id if data.cursor_id
      @curveId = data.curve_id if data.curve_id
      @cursorColor = data.cursor_color if data.cursor_color
      @cursorImg = data.cursor_img if data.cursor_img

      if(data.cursor_size)
        @height = data.cursor_size
        @width = data.cursor_size

    #TODO: make max dynamic
    @max = 425 if !data.max

  init: () ->
    @value = 0
    @min = 0
    @max = 100
    @height = 20
    @width = 15
    @x = 0
    @y = 0
    @parent = undefined
    @cursorId = undefined
    @curveId = undefined
    @rect = undefined
    @cursorColor = "#99CE1F"
    @cursorImg = undefined


  setValue: (v) ->
    @value = v
    @x = @value

  setMax: (v) ->
    @max = v

  xIsBetween: (x1, x2)->
    return (@x >= x1 && @x < x2)

  constraint: (curve) ->

    if(@x - @width/2 < @min)
      @x = @min+ @width/2


    if(@x + @width/2 > @max)
      @x = @max - @width/2

    @y = Curve.curveYAtX(curve, @x)


### VIEWS ####

#Curve

<div class="minicurve-container" >

  <div id="{{curve.curveId}}Container" class="svg-container">

    <div class="label-div" >
      <span id="curve-label" class="curve-label">
        {{curve.curveLabel | translate}}
      </span>
    </div>


    <svg  height="310" width="425"  x="0px" y="0px" id="{{curve.curveId}}SVG">

      <defs>

        <mask id="{{curve.curveBMask.maskId}}">
          <curve-mask curve="curve" standalone="false" maskobj="curve.curveBMask"></curve-mask>
        </mask>

        <mask id="{{curve.curveOMask.maskId}}">
          <curve-mask curve="curve" standalone="true" maskobj="curve.curveOMask"></curve-mask>
        </mask>

        <marker id="markerArrow" markerWidth="5" markerHeight="5" refX="0" refY="2.5"
                orient="auto" markerUnits="strokeWidth">
          <path d="m0.03774,0.03774 l4.9434,2.37736 l-4.90566,2.49057 l-0.03774,-4.86792 z" style="fill: #000000;" />
        </marker>

      </defs>

      <!-- MAIN CURVE -->
      <path id="{{curve.curveId}}"  fill="none" stroke="#9B9B9B" stroke-width="1" d="{{curve.curveData}}"/>

      <!-- SECONDARY BLACK CURVE -->
      <path id="{{curve.curveId}}B" stroke-width="2"  fill="none" stroke="#000" d="{{curve.curveData}}" mask="url(#{{curve.curveBMask.maskId}})"/>


      <!-- SECONDARY ORANGE CURVE -->
      <path id="{{curve.curveId}}O" stroke-width="3"  fill="none" stroke="#FEAD00" d="{{curve.curveData}}" mask="url(#{{curve.curveOMask.maskId}})"/>


      <g fill="none" stroke="black" stroke-width="2" >
        <line ng-if="curve.curveId == 'profitsCurve'" x1="4" y1="145" x2="415" y2="145" style="marker-end:
        url(#markerArrow);"></line>
        <line ng-if="curve.curveId != 'profitsCurve'" x1="4" y1="290" x2="415" y2="290" style="marker-end: url(#markerArrow);"></line>
        <line x1="4" y1="290" x2="4" y2="10" style="marker-end: url(#markerArrow);"></line>
      </g>

      <!-- CURSORS -->
      <curve-cursor curve="curve" cursor="curve.cursorA"></curve-cursor>
      <curve-cursor curve="curve" cursor="curve.cursorB"></curve-cursor>

    </svg>

  </div>

</div>

#Cursor

<g id="{{cursor.cursorId}}">
    <defs>
        <pattern id="{{cursor.cursorId}}img" width="25" height="25">
            <image xlink:href="{{cursor.cursorImg}}" x="0" y="0" width="25" height="25" />
        </pattern>
    </defs>

    <rect
            x="{{cursor.x-cursor.width/2}}"
            y="{{cursor.y-cursor.height/2}}"
            width="{{cursor.width}}"
            height="{{cursor.height}}"
            fill="{{cursor.cursorImg ? 'url(#' + cursor.cursorId + 'img)' : cursor.cursorColor}}"
            stroke="#000"
            stroke-width="1px"
            >
    </rect>

</g>


### DIRECTIVES ###

.directive('curveCursor', ["$timeout",  function($timeout) {
    return {
        restrict: 'E',
        replace: true,
        transclude: true,
        templateNamespace: 'svg',
        scope: {
            curve: '=curve',
            cursor: '=cursor'
        },
        templateUrl: 'views/system_maturity/_curve_cursor.html',
        link: function(scope, elem, attr){

          $timeout(function(){

            if(scope.curve.curveId == "MGCurve"){
              scope.$parent.$emit("CursorReady", {cursor: scope.cursor, curve: scope.curve})
            }else{
              scope.$parent.$parent.$emit("CursorReady", {cursor: scope.cursor, curve: scope.curve})
            }

          })
        }
    };
}]);

.directive('miniGraph', ["$timeout", function($timeout) {
  return {
    restrict: 'E',
    replace: true,
    transclude: true,
    templateUrl: "views/system_maturity/_mini_graph.html"
  };
}]);


### CONTROLLER  ###

  $scope.$on("CursorReady", (evt, data)->

    cursor = data.cursor
    curve = data.curve

    domCurve = document.getElementById(cursor.curveId)

    cursor.max = domCurve.getBoundingClientRect().width + domCurve.getBBox().x
    cursor.min = domCurve.getBBox().x

    scope = $scope

    prevPoint = {x:0, y:0}

    cursor.constraint(cursor.curveId)
    $scope.updateOMasks(cursor, false)
    $scope.constraintPF(cursor)

    $('#'+cursor.cursorId).draggable

      start: (event, ui) ->

        event.stopPropagation()
        prevPoint =
          x: parseFloat(event.clientX)
          y: parseFloat(event.clientY)

      drag: (event, ui) ->
        cursor.x = (cursor.x + parseFloat(event.clientX) - prevPoint.x )
        event.stopPropagation()
        prevPoint =
          x: parseFloat(event.clientX)
          y: parseFloat(event.clientY)

        cursor.constraint(cursor.curveId)
        scope.updateOMasks(cursor, true)
        scope.constraintPF(cursor)

        scope.$apply()

  )
Flags: needinfo?(contact)
Looks broadly OK, We need something we can run though, e.g. a jsfiddle
Component: Untriaged → Event Handling
Product: Firefox → Core
I tested on Windows 7 on Firefox 41.0.2 the bug can also be reproduce here.

Steps to reproduce:

1 Enter on this link  http://plnkr.co/edit/BQEqTDVLIE95pQ3QzClv?p=preview
2 Try to follow the path with the cursor.

Actual results: The cursor doesn't follow the path.

Expected results: The cursor should follow the path.
Status: UNCONFIRMED → NEW
Ever confirmed: true
Hi,

is it possible to have an update of that ticket ?
Really needs a much much much smaller testcase in order to progress. Ideally without angular or jQuery.
the point is that we don't have any issues in other browsers ! only with Firefox.
So it doesn't come from angular or jQuery.
So could you please confirm us that problem comes from Firefox javascript's engine now ?
Regards,
Not till you address comment 7. Without that I've no idea what the problem is.
Whiteboard: [platform-rel-jQuery]
platform-rel: --- → ?
smaug, is this an events bug?
platform-rel: ? → +
Flags: needinfo?(bugs)
Severity: blocker → normal
Flags: needinfo?(bugs)
Priority: P1 → --
Flags: needinfo?(bugs)
Stone, do you have time to take a quick look here?
Flags: needinfo?(sshih)
I might be able to check it on next Tuesday. Keep ni flag to remind me.
Rank: 25
The links provided in this bug are not working. Looks like there are something wrong of droplr. I'd re-try them later.
Clear ni flag since these links are no longer valid and I have no idea about how to reproduce the problem.
Flags: needinfo?(sshih)
I could still reproduce the issue with the link from comment 5.
Oh, I missed this one. This one is valid and I'll check it.
It hits the assertion in [1] when I try to reproduce the bug (dragging the green box).

[1] http://searchfox.org/mozilla-central/source/layout/svg/nsSVGContainerFrame.cpp#391
Moving to Core:SVG according to comment 17.
Component: Event Handling → SVG
We looked at this during partner triage today and aren't sure how badly it hurts users or what next step should be... Andrew (and Olli) any ideas? Do we think this is an events bug?
Flags: needinfo?(overholt)
It's almost certainly a bug in their code somewhere. Most probably some co-ordinate system transformation is invalid.
or in our code. Nothing particularly hints about events bug. Some coordinates are going wrong somewhere. Minimal testcase would be really nice.
(In reply to Olli Pettay [:smaug] from comment #21)
> Some coordinates are going wrong somewhere. Minimal testcase would be really nice.

Any chance for a smaller testcase?
Flags: needinfo?(overholt) → needinfo?(contact)
I tried that in comment 7, comment 8 and comment 9 without much response.

A minimal testcase would be really nice here.
The example in comment 5 is rather large.

Flags: needinfo?(bugs)
Severity: normal → S3

Clear a needinfo that is pending on an inactive user.

Inactive users most likely will not respond; if the missing information is essential and cannot be collected another way, the bug maybe should be closed as INCOMPLETE.

For more information, please visit auto_nag documentation.

Flags: needinfo?(contact)
Status: NEW → RESOLVED
Closed: 2 years ago
Resolution: --- → INCOMPLETE
You need to log in before you can comment on or make changes to this bug.