pointer-events.js (10145B)
1 SimpleTest.waitForExplicitFinish(); 2 3 var pointer_events_values = [ 4 "auto", 5 "visiblePainted", 6 "visibleFill", 7 "visibleStroke", 8 "visible", 9 "painted", 10 "fill", 11 "stroke", 12 "all", 13 "none", 14 ]; 15 16 var paint_values = ["blue", "transparent", "none"]; 17 18 var opacity_values = ["1", "0.5", "0"]; 19 20 var visibility_values = ["visible", "hidden", "collapse"]; 21 22 /** 23 * List of attributes and various values for which we want to test permutations 24 * when hit testing a pointer event that is over an element's fill area, 25 * stroke area, or both (where they overlap). 26 * 27 * We're using an array of objects so that we have control over the order in 28 * which permutations are tested. 29 * 30 * TODO: test the effect of clipping, masking, filters, markers, etc. 31 */ 32 var hit_test_inputs = { 33 fill: [ 34 { name: "pointer-events", values: pointer_events_values }, 35 { name: "fill", values: paint_values }, 36 { name: "fill-opacity", values: opacity_values }, 37 { name: "opacity", values: opacity_values }, 38 { name: "visibility", values: visibility_values }, 39 ], 40 stroke: [ 41 { name: "pointer-events", values: pointer_events_values }, 42 { name: "stroke", values: paint_values }, 43 { name: "stroke-opacity", values: opacity_values }, 44 { name: "opacity", values: opacity_values }, 45 { name: "visibility", values: visibility_values }, 46 ], 47 both: [ 48 { name: "pointer-events", values: pointer_events_values }, 49 { name: "fill", values: paint_values }, 50 { name: "fill-opacity", values: opacity_values }, 51 { name: "stroke", values: paint_values }, 52 { name: "stroke-opacity", values: opacity_values }, 53 { name: "opacity", values: opacity_values }, 54 { name: "visibility", values: visibility_values }, 55 ], 56 }; 57 58 /** 59 * The following object contains a list of 'pointer-events' property values, 60 * each with an object detailing the conditions under which the fill and stroke 61 * of a graphical object will intercept pointer events for the given value. If 62 * the object contains a 'fill-intercepts-iff' property then the fill is 63 * expected to intercept pointer events for that value of 'pointer-events' if 64 * and only if the conditions listed in the 'fill-intercepts-iff' object are 65 * met. If there are no conditions in the 'fill-intercepts-iff' object then the 66 * fill should always intercept pointer events. However, if the 67 * 'fill-intercepts-iff' property is not set at all then it indicates that the 68 * fill should never intercept pointer events. The same rules apply for 69 * 'stroke-intercepts-iff'. 70 * 71 * If an attribute name in the conditions list is followed by the "!" 72 * character then the requirement for a hit is that its value is NOT any 73 * of the values listed in the given array. 74 */ 75 var hit_conditions = { 76 auto: { 77 "fill-intercepts-iff": { 78 visibility: ["visible"], 79 "fill!": ["none"], 80 }, 81 "stroke-intercepts-iff": { 82 visibility: ["visible"], 83 "stroke!": ["none"], 84 }, 85 }, 86 visiblePainted: { 87 "fill-intercepts-iff": { 88 visibility: ["visible"], 89 "fill!": ["none"], 90 }, 91 "stroke-intercepts-iff": { 92 visibility: ["visible"], 93 "stroke!": ["none"], 94 }, 95 }, 96 visibleFill: { 97 "fill-intercepts-iff": { 98 visibility: ["visible"], 99 }, 100 // stroke never intercepts pointer events 101 }, 102 visibleStroke: { 103 // fill never intercepts pointer events 104 "stroke-intercepts-iff": { 105 visibility: ["visible"], 106 }, 107 }, 108 visible: { 109 "fill-intercepts-iff": { 110 visibility: ["visible"], 111 }, 112 "stroke-intercepts-iff": { 113 visibility: ["visible"], 114 }, 115 }, 116 painted: { 117 "fill-intercepts-iff": { 118 "fill!": ["none"], 119 }, 120 "stroke-intercepts-iff": { 121 "stroke!": ["none"], 122 }, 123 }, 124 fill: { 125 "fill-intercepts-iff": { 126 // fill always intercepts pointer events 127 }, 128 // stroke never intercepts pointer events 129 }, 130 stroke: { 131 // fill never intercepts pointer events 132 "stroke-intercepts-iff": { 133 // stroke always intercepts pointer events 134 }, 135 }, 136 all: { 137 "fill-intercepts-iff": { 138 // fill always intercepts pointer events 139 }, 140 "stroke-intercepts-iff": { 141 // stroke always intercepts pointer events 142 }, 143 }, 144 none: { 145 // neither fill nor stroke intercept pointer events 146 }, 147 }; 148 149 // bit flags 150 var POINT_OVER_FILL = 0x1; 151 var POINT_OVER_STROKE = 0x2; 152 153 /** 154 * Examine the element's attribute values and, based on the area(s) of the 155 * element that the pointer event is over (fill and/or stroke areas), return 156 * true if the element is expected to intercept the event, otherwise false. 157 */ 158 function hit_expected( 159 element, 160 over /* bit flags indicating which area(s) of the element the pointer is over */ 161 ) { 162 function expect_hit(target) { 163 var intercepts_iff = 164 hit_conditions[element.getAttribute("pointer-events")][ 165 target + "-intercepts-iff" 166 ]; 167 168 if (!intercepts_iff) { 169 return false; // never intercepts events 170 } 171 172 for (var attr in intercepts_iff) { 173 var vals = intercepts_iff[attr]; // must get this before we adjust 'attr' 174 var invert = false; 175 if (attr.substr(-1) == "!") { 176 invert = true; 177 attr = attr.substr(0, attr.length - 1); 178 } 179 var match = vals.indexOf(element.getAttribute(attr)) > -1; 180 if (invert) { 181 match = !match; 182 } 183 if (!match) { 184 return false; 185 } 186 } 187 188 return true; 189 } 190 191 return ( 192 ((over & POINT_OVER_FILL) != 0 && expect_hit("fill")) || 193 ((over & POINT_OVER_STROKE) != 0 && expect_hit("stroke")) 194 ); 195 } 196 197 function for_all_permutations(inputs, callback) { 198 var current_permutation = arguments[2] || {}; 199 var index = arguments[3] || 0; 200 201 if (index < inputs.length) { 202 var name = inputs[index].name; 203 var values = inputs[index].values; 204 for (var i = 0; i < values.length; ++i) { 205 current_permutation[name] = values[i]; 206 for_all_permutations(inputs, callback, current_permutation, index + 1); 207 } 208 return; 209 } 210 211 callback(current_permutation); 212 } 213 214 function make_log_msg(over, tag, attributes) { 215 var target; 216 if (over == (POINT_OVER_FILL | POINT_OVER_STROKE)) { 217 target = "fill and stroke"; 218 } else if (over == POINT_OVER_FILL) { 219 target = "fill"; 220 } else if (over == POINT_OVER_STROKE) { 221 target = "stroke"; 222 } else { 223 throw new Error("unexpected bit combination in 'over'"); 224 } 225 var msg = 226 "Check if events are intercepted at a point over the " + 227 target + 228 " on <" + 229 tag + 230 "> for"; 231 for (var attr in attributes) { 232 msg += " " + attr + "=" + attributes[attr]; 233 } 234 return msg; 235 } 236 237 var dx, dy; // offset of <svg> element from pointer coordinates origin 238 239 function test_element( 240 id, 241 x, 242 y, 243 over /* bit flags indicating which area(s) of the element the pointer is over */ 244 ) { 245 var element = document.getElementById(id); 246 var tag = element.tagName; 247 248 function test_permutation(attributes) { 249 for (var attr in attributes) { 250 element.setAttribute(attr, attributes[attr]); 251 } 252 var hits = document.elementFromPoint(dx + x, dy + y) == element; 253 var msg = make_log_msg(over, tag, attributes); 254 255 is(hits, hit_expected(element, over), msg); 256 } 257 258 var inputs; 259 if (over == (POINT_OVER_FILL | POINT_OVER_STROKE)) { 260 inputs = hit_test_inputs.both; 261 } else if (over == POINT_OVER_FILL) { 262 inputs = hit_test_inputs.fill; 263 } else if (over == POINT_OVER_STROKE) { 264 inputs = hit_test_inputs.stroke; 265 } else { 266 throw new Error("unexpected bit combination in 'over'"); 267 } 268 269 for_all_permutations(inputs, test_permutation); 270 271 // To reduce the chance of bogus results in subsequent tests: 272 element.setAttribute("fill", "none"); 273 element.setAttribute("stroke", "none"); 274 } 275 276 function run_tests(subtest) { 277 var div = document.getElementById("div"); 278 dx = div.offsetLeft; 279 dy = div.offsetTop; 280 281 // Run the test with only a subset of pointer-events values, to avoid 282 // running over the mochitest time limit. The subtest argument indicates 283 // whether to use the first half of the pointer-events values (0) 284 // or the second half (1). 285 var partition = Math.floor(pointer_events_values.length / 2); 286 switch (subtest) { 287 case 0: 288 pointer_events_values.splice(partition); 289 break; 290 case 1: 291 pointer_events_values.splice(0, partition); 292 break; 293 case 2: 294 throw new Error("unexpected subtest number"); 295 } 296 297 test_element("rect", 30, 30, POINT_OVER_FILL); 298 test_element("rect", 5, 5, POINT_OVER_STROKE); 299 300 // The SVG 1.1 spec essentially says that, for text, hit testing is done 301 // against the character cells of the text, and not the fill and stroke as 302 // you might expect for a normal graphics element like <path>. See the 303 // paragraph starting "For text elements..." in this section: 304 // 305 // http://www.w3.org/TR/SVG11/interact.html#PointerEventsProperty 306 // 307 // This requirement essentially means that for the purposes of hit testing 308 // the fill and stroke areas are the same area - the character cell. (At 309 // least until we support having any fill or stroke that lies outside the 310 // character cells intercept events like Opera does - see below.) Thus, for 311 // text, when a pointer event is over a character cell it is essentially over 312 // both the fill and stroke at the same time. That's the reason we pass both 313 // the POINT_OVER_FILL and POINT_OVER_STROKE bits in test_element's 'over' 314 // argument below. It's also the reason why we only test one point in the 315 // text rather than having separate tests for fill and stroke. 316 // 317 // For hit testing of text, Opera essentially treats fill and stroke like it 318 // would on any normal element, but it adds the character cells of glyhs to 319 // both the glyphs' fill AND stroke. I think this is what we should do too. 320 // It's compatible with the letter of the SVG 1.1 rules, and it allows any 321 // parts of a glyph that are outside the glyph's character cells to also 322 // intercept events in the normal way. When we make that change we'll be able 323 // to add separate fill and stroke tests for text below. 324 325 test_element("text", 210, 30, POINT_OVER_FILL | POINT_OVER_STROKE); 326 327 SimpleTest.finish(); 328 }