@workiom/frappe-gantt 1.0.19 → 1.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/arrow.js CHANGED
@@ -1,12 +1,14 @@
1
1
  import { createSVG } from './svg_utils';
2
2
 
3
3
  export default class Arrow {
4
- constructor(gantt, from_task, to_task) {
4
+ constructor(gantt, from_task, to_task, dependency_type) {
5
5
  this.gantt = gantt;
6
6
  this.from_task = from_task;
7
7
  this.to_task = to_task;
8
+ this.dependency_type = dependency_type;
8
9
  this.is_critical = this.check_critical_path();
9
10
  this.is_invalid = this.check_invalid_dependency();
11
+ this.is_hovered = false;
10
12
 
11
13
  this.calculate_path();
12
14
  this.draw();
@@ -21,18 +23,11 @@ export default class Arrow {
21
23
  }
22
24
 
23
25
  check_invalid_dependency() {
24
- const dependencies_type = this.to_task.task.dependencies_type ||
25
- this.gantt.options.dependencies_type;
26
-
27
- // Fixed dependencies use old logic
28
- if (dependencies_type === 'fixed') {
29
- return this.to_task.$bar.getX() < this.from_task.$bar.getX();
30
- }
31
-
26
+ const dependency_type = this.dependency_type;
32
27
  const parent_task = this.from_task.task;
33
28
  const child_task = this.to_task.task;
34
29
 
35
- switch(dependencies_type) {
30
+ switch(dependency_type) {
36
31
  case 'finish-to-start':
37
32
  // Child task cannot start before parent finishes
38
33
  return child_task._start < parent_task._end;
@@ -54,81 +49,139 @@ export default class Arrow {
54
49
  }
55
50
 
56
51
  calculate_path() {
57
- let start_x =
58
- this.from_task.$bar.getX() + this.from_task.$bar.getWidth() / 2;
59
-
60
- const condition = () =>
61
- this.to_task.$bar.getX() < start_x + this.gantt.options.padding &&
62
- start_x > this.from_task.$bar.getX() + this.gantt.options.padding;
63
-
64
- while (condition()) {
65
- start_x -= 10;
52
+ const opt = this.gantt.options;
53
+ const cfg = this.gantt.config;
54
+ const curve = opt.arrow_curve;
55
+ const padding = opt.padding;
56
+
57
+ // Anchor x positions
58
+ const right_A = this.from_task.$bar.getX() + this.from_task.$bar.getWidth();
59
+ const left_A = this.from_task.$bar.getX();
60
+ const right_B = this.to_task.$bar.getX() + this.to_task.$bar.getWidth();
61
+ const left_B = this.to_task.$bar.getX();
62
+
63
+ // Anchor y positions — vertical center of each bar row
64
+ const row_center = (task) =>
65
+ cfg.header_height +
66
+ opt.bar_height / 2 +
67
+ (opt.padding + opt.bar_height) * task.task._index +
68
+ opt.padding / 2;
69
+
70
+ const y_A = row_center(this.from_task);
71
+ const y_B = row_center(this.to_task);
72
+ const y_mid = (y_A + y_B) / 2;
73
+
74
+ switch (this.dependency_type) {
75
+ case 'finish-to-start':
76
+ this.path = this._path_finish_to_start(
77
+ right_A, left_A, right_B, left_B, y_A, y_B, y_mid, padding, curve
78
+ );
79
+ break;
80
+ case 'start-to-start':
81
+ this.path = this._path_start_to_start(
82
+ left_A, left_B, y_A, y_B, padding, curve
83
+ );
84
+ break;
85
+ case 'finish-to-finish':
86
+ this.path = this._path_finish_to_finish(
87
+ right_A, right_B, y_A, y_B, padding, curve
88
+ );
89
+ break;
90
+ case 'start-to-finish':
91
+ this.path = this._path_start_to_finish(
92
+ left_A, right_B, y_A, y_B, y_mid, padding, curve
93
+ );
94
+ break;
95
+ default:
96
+ this.path = this._path_finish_to_start(
97
+ right_A, left_A, right_B, left_B, y_A, y_B, y_mid, padding, curve
98
+ );
66
99
  }
67
- start_x -= 10;
68
-
69
- let start_y =
70
- this.gantt.config.header_height +
71
- this.gantt.options.bar_height +
72
- (this.gantt.options.padding + this.gantt.options.bar_height) *
73
- this.from_task.task._index +
74
- this.gantt.options.padding / 2;
75
-
76
- let end_x = this.to_task.$bar.getX() - 13;
77
- let end_y =
78
- this.gantt.config.header_height +
79
- this.gantt.options.bar_height / 2 +
80
- (this.gantt.options.padding + this.gantt.options.bar_height) *
81
- this.to_task.task._index +
82
- this.gantt.options.padding / 2;
83
-
84
- const from_is_below_to =
85
- this.from_task.task._index > this.to_task.task._index;
86
-
87
- let curve = this.gantt.options.arrow_curve;
88
- const clockwise = from_is_below_to ? 1 : 0;
89
- let curve_y = from_is_below_to ? -curve : curve;
90
-
91
- if (
92
- this.to_task.$bar.getX() <=
93
- this.from_task.$bar.getX() + this.gantt.options.padding
94
- ) {
95
- let down_1 = this.gantt.options.padding / 2 - curve;
96
- if (down_1 < 0) {
97
- down_1 = 0;
98
- curve = this.gantt.options.padding / 2;
99
- curve_y = from_is_below_to ? -curve : curve;
100
- }
101
- const down_2 =
102
- this.to_task.$bar.getY() +
103
- this.to_task.$bar.getHeight() / 2 -
104
- curve_y;
105
- const left = this.to_task.$bar.getX() - this.gantt.options.padding;
106
- this.path = `
107
- M ${start_x} ${start_y}
108
- v ${down_1}
109
- a ${curve} ${curve} 0 0 1 ${-curve} ${curve}
110
- H ${left}
111
- a ${curve} ${curve} 0 0 ${clockwise} ${-curve} ${curve_y}
112
- V ${down_2}
113
- a ${curve} ${curve} 0 0 ${clockwise} ${curve} ${curve_y}
114
- L ${end_x} ${end_y}
115
- m -5 -5
116
- l 5 5
117
- l -5 5`;
118
- } else {
119
- if (end_x < start_x + curve) curve = end_x - start_x;
120
-
121
- let offset = from_is_below_to ? end_y + curve : end_y - curve;
122
-
123
- this.path = `
124
- M ${start_x} ${start_y}
125
- V ${offset}
126
- a ${curve} ${curve} 0 0 ${clockwise} ${curve} ${curve}
127
- L ${end_x} ${end_y}
128
- m -5 -5
129
- l 5 5
130
- l -5 5`;
100
+ }
101
+
102
+ _path_finish_to_start(right_A, left_A, right_B, left_B, y_A, y_B, y_mid, padding, curve) {
103
+ const x_right = right_A + padding;
104
+
105
+ if (x_right < left_B) {
106
+ // Case 1: space between tasks — 3 segments: right, down, right
107
+ return `
108
+ M ${right_A} ${y_A}
109
+ H ${x_right - curve}
110
+ a ${curve} ${curve} 0 0 1 ${curve} ${curve}
111
+ V ${y_B - curve}
112
+ a ${curve} ${curve} 0 0 0 ${curve} ${curve}
113
+ H ${left_B}
114
+ m -5 -5 l 5 5 l -5 5`;
131
115
  }
116
+
117
+ // Case 2: overlap — 5 segments: right, down, left, down, right
118
+ const x_left = left_B - padding;
119
+ return `
120
+ M ${right_A} ${y_A}
121
+ H ${x_right - curve}
122
+ a ${curve} ${curve} 0 0 1 ${curve} ${curve}
123
+ V ${y_mid - curve}
124
+ a ${curve} ${curve} 0 0 1 ${-curve} ${curve}
125
+ H ${x_left + curve}
126
+ a ${curve} ${curve} 0 0 0 ${-curve} ${curve}
127
+ V ${y_B - curve}
128
+ a ${curve} ${curve} 0 0 0 ${curve} ${curve}
129
+ H ${left_B}
130
+ m -5 -5 l 5 5 l -5 5`;
131
+ }
132
+
133
+ _path_start_to_start(left_A, left_B, y_A, y_B, padding, curve) {
134
+ const x_left = Math.min(left_A, left_B) - padding;
135
+
136
+ return `
137
+ M ${left_A} ${y_A}
138
+ H ${x_left + curve}
139
+ a ${curve} ${curve} 0 0 0 ${-curve} ${curve}
140
+ V ${y_B - curve}
141
+ a ${curve} ${curve} 0 0 0 ${curve} ${curve}
142
+ H ${left_B}
143
+ m -5 -5 l 5 5 l -5 5`;
144
+ }
145
+
146
+ _path_finish_to_finish(right_A, right_B, y_A, y_B, padding, curve) {
147
+ const x_right = Math.max(right_A, right_B) + padding;
148
+
149
+ return `
150
+ M ${right_A} ${y_A}
151
+ H ${x_right - curve}
152
+ a ${curve} ${curve} 0 0 1 ${curve} ${curve}
153
+ V ${y_B - curve}
154
+ a ${curve} ${curve} 0 0 1 ${-curve} ${curve}
155
+ H ${right_B}
156
+ m 5 -5 l -5 5 l 5 5`;
157
+ }
158
+
159
+ _path_start_to_finish(left_A, right_B, y_A, y_B, y_mid, padding, curve) {
160
+ const x_left = left_A - padding;
161
+ const x_right = right_B + padding;
162
+
163
+ return `
164
+ M ${left_A} ${y_A}
165
+ H ${x_left + curve}
166
+ a ${curve} ${curve} 0 0 0 ${-curve} ${curve}
167
+ V ${y_mid - curve}
168
+ a ${curve} ${curve} 0 0 0 ${curve} ${curve}
169
+ H ${x_right - curve}
170
+ a ${curve} ${curve} 0 0 1 ${curve} ${curve}
171
+ V ${y_B - curve}
172
+ a ${curve} ${curve} 0 0 1 ${-curve} ${curve}
173
+ H ${right_B}
174
+ m 5 -5 l -5 5 l 5 5`;
175
+ }
176
+
177
+ _get_connected_bars() {
178
+ const from_id = this.from_task.task.id;
179
+ const to_id = this.to_task.task.id;
180
+ return Array.from(
181
+ this.gantt.$svg.querySelectorAll(
182
+ `[data-id="${CSS.escape(from_id)}"], [data-id="${CSS.escape(to_id)}"]`
183
+ )
184
+ );
132
185
  }
133
186
 
134
187
  draw() {
@@ -145,11 +198,44 @@ export default class Arrow {
145
198
  'data-to': this.to_task.task.id,
146
199
  class: arrowClass,
147
200
  });
201
+
202
+ // Wide transparent path for easier mouse targeting
203
+ this.hit_element = createSVG('path', {
204
+ d: this.path,
205
+ stroke: 'transparent',
206
+ 'stroke-width': 15,
207
+ fill: 'none',
208
+ style: 'pointer-events: stroke; cursor: pointer;',
209
+ });
210
+
211
+ this.hit_element.addEventListener('mouseenter', () => {
212
+ this.is_hovered = true;
213
+ this.element.classList.add('arrow-hover');
214
+ const bar_class = this.is_invalid
215
+ ? 'bar-arrow-invalid'
216
+ : this.is_critical
217
+ ? 'bar-arrow-critical'
218
+ : 'bar-arrow-hover';
219
+ this._get_connected_bars().forEach(el => {
220
+ const bar = el.querySelector('.bar');
221
+ if (bar) bar.classList.add(bar_class);
222
+ });
223
+ });
224
+
225
+ this.hit_element.addEventListener('mouseleave', () => {
226
+ this.is_hovered = false;
227
+ this.element.classList.remove('arrow-hover');
228
+ this._get_connected_bars().forEach(el => {
229
+ const bar = el.querySelector('.bar');
230
+ if (bar) bar.classList.remove('bar-arrow-hover', 'bar-arrow-critical', 'bar-arrow-invalid');
231
+ });
232
+ });
148
233
  }
149
234
 
150
235
  update() {
151
236
  this.calculate_path();
152
237
  this.element.setAttribute('d', this.path);
238
+ this.hit_element.setAttribute('d', this.path);
153
239
 
154
240
  // Update invalid state
155
241
  this.is_invalid = this.check_invalid_dependency();
@@ -161,6 +247,7 @@ export default class Arrow {
161
247
  } else if (this.is_critical) {
162
248
  arrowClass = 'arrow-critical';
163
249
  }
164
- this.element.setAttribute('class', arrowClass);
250
+ if (this.is_hovered) arrowClass += ' arrow-hover';
251
+ this.element.setAttribute('class', arrowClass.trim());
165
252
  }
166
253
  }
package/src/bar.js CHANGED
@@ -655,76 +655,6 @@ export default class Bar {
655
655
  this.update_arrow_position();
656
656
  }
657
657
 
658
- validate_dependency_constraints(new_x, new_width = null) {
659
- const dependencies_type = this.task.dependencies_type || this.gantt.options.dependencies_type;
660
-
661
- // For fixed dependencies, use old validation logic
662
- if (dependencies_type === 'fixed') {
663
- const xs = this.task.dependencies.map((dep) => {
664
- return this.gantt.get_bar(dep).$bar.getX();
665
- });
666
- return xs.reduce((prev, curr) => {
667
- return prev && new_x >= curr;
668
- }, true);
669
- }
670
-
671
- // Calculate what the new dates would be
672
- const new_start_x = new_x / this.gantt.config.column_width;
673
- const new_start = date_utils.add(
674
- this.gantt.gantt_start,
675
- new_start_x * this.gantt.config.step,
676
- this.gantt.config.unit
677
- );
678
-
679
- const bar_width = new_width || this.$bar.getWidth();
680
- const width_in_units = bar_width / this.gantt.config.column_width;
681
- const new_end = date_utils.add(
682
- new_start,
683
- width_in_units * this.gantt.config.step,
684
- this.gantt.config.unit
685
- );
686
-
687
- // Check constraints for each parent dependency
688
- for (const dep_id of this.task.dependencies) {
689
- const parent_bar = this.gantt.get_bar(dep_id);
690
- if (!parent_bar) continue;
691
-
692
- const parent_task = parent_bar.task;
693
-
694
- switch(dependencies_type) {
695
- case 'finish-to-start':
696
- // This task cannot start before parent finishes
697
- if (new_start < parent_task._end) {
698
- return false;
699
- }
700
- break;
701
-
702
- case 'start-to-start':
703
- // This task cannot start before parent starts
704
- if (new_start < parent_task._start) {
705
- return false;
706
- }
707
- break;
708
-
709
- case 'finish-to-finish':
710
- // This task cannot finish before parent finishes
711
- if (new_end < parent_task._end) {
712
- return false;
713
- }
714
- break;
715
-
716
- case 'start-to-finish':
717
- // This task cannot finish before parent starts
718
- if (new_end < parent_task._start) {
719
- return false;
720
- }
721
- break;
722
- }
723
- }
724
-
725
- return true;
726
- }
727
-
728
658
  update_label_position_on_horizontal_scroll({ x, sx }) {
729
659
  const container = this.gantt.$container;
730
660
  const label = this.group.querySelector('.bar-label');
package/src/defaults.js CHANGED
@@ -116,7 +116,8 @@ const DEFAULT_OPTIONS = {
116
116
  column_width: null,
117
117
  critical_path: false,
118
118
  date_format: 'YYYY-MM-DD HH:mm',
119
- dependencies_type: 'fixed',
119
+ dependencies_type: 'finish-to-start',
120
+ dependency_shifting: 'none', // 'none' | 'maintain_buffer_all' | 'maintain_buffer_downstream' | 'consume_buffer'
120
121
  upper_header_height: 45,
121
122
  lower_header_height: 30,
122
123
  snap_at: null,
@@ -126,7 +127,6 @@ const DEFAULT_OPTIONS = {
126
127
  isRTL: false,
127
128
  language: 'en',
128
129
  lines: 'both',
129
- move_dependencies: true,
130
130
  padding: 18,
131
131
  popup: (ctx) => {
132
132
  ctx.set_title(ctx.task.name);
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Computes how much each dependent task should shift after a task is dragged.
3
+ *
4
+ * @param {object[]} tasks - Full gantt tasks array (each with _start, _end as Date, id, dependencies)
5
+ * @param {string} movedTaskId - ID of the task that was dragged
6
+ * @param {number} deltaMs - Net movement in milliseconds (negative = moved earlier)
7
+ * @param {string} mode - Value of options.dependency_shifting
8
+ * @returns {Map<string, number>} taskId → deltaMs to apply (movedTaskId is excluded)
9
+ */
10
+ export function compute_dependency_shifts(tasks, movedTaskId, deltaMs, mode, direction = 'downstream') {
11
+ if (mode === 'none' || deltaMs === 0) return new Map();
12
+
13
+ if (!['upstream', 'downstream', 'both'].includes(direction)) {
14
+ console.warn(`[frappe-gantt] compute_dependency_shifts: unknown direction "${direction}", falling back to "downstream"`);
15
+ direction = 'downstream';
16
+ }
17
+
18
+ // Build graph
19
+ const taskById = new Map();
20
+ const predecessors = new Map(); // taskId -> [{ id, type }]
21
+ const successors = new Map(); // taskId -> [{ id, type }]
22
+
23
+ for (const task of tasks) {
24
+ if (task._has_no_dates) continue;
25
+ taskById.set(task.id, task);
26
+ predecessors.set(task.id, []);
27
+ successors.set(task.id, []);
28
+ }
29
+
30
+ for (const task of tasks) {
31
+ if (task._has_no_dates) continue;
32
+ for (const dep of task.dependencies || []) {
33
+ if (!taskById.has(dep.id)) continue;
34
+ const type = dep.type || 'finish-to-start';
35
+ predecessors.get(task.id).push({ id: dep.id, type });
36
+ successors.get(dep.id).push({ id: task.id, type });
37
+ }
38
+ }
39
+
40
+ if (mode === 'maintain_buffer_all') {
41
+ const bidirectional = direction === 'both';
42
+ const upstream_only = direction === 'upstream';
43
+ return _bfs_shift(movedTaskId, deltaMs, predecessors, successors, bidirectional, upstream_only);
44
+ }
45
+ if (mode === 'maintain_buffer_downstream') {
46
+ const bidirectional = direction === 'both';
47
+ const upstream_only = direction === 'upstream';
48
+ return _bfs_shift(movedTaskId, deltaMs, predecessors, successors, bidirectional, upstream_only);
49
+ }
50
+ if (mode === 'consume_buffer') {
51
+ return _consume_buffer_shift(movedTaskId, deltaMs, taskById, predecessors, successors, direction);
52
+ }
53
+
54
+ return new Map();
55
+ }
56
+
57
+ /**
58
+ * BFS traversal: applies the same deltaMs to every reachable task.
59
+ * bidirectional=true visits both upstream and downstream.
60
+ * upstream_only=true (and bidirectional=false) visits predecessors only.
61
+ * Default (both false) visits successors only.
62
+ */
63
+ function _bfs_shift(movedTaskId, deltaMs, predecessors, successors, bidirectional, upstream_only) {
64
+ const result = new Map();
65
+ const visited = new Set([movedTaskId]);
66
+ const queue = [];
67
+
68
+ const enqueue = (neighbors) => {
69
+ for (const { id } of neighbors) {
70
+ if (!visited.has(id)) {
71
+ visited.add(id);
72
+ queue.push(id);
73
+ }
74
+ }
75
+ };
76
+
77
+ if (upstream_only && !bidirectional) {
78
+ enqueue(predecessors.get(movedTaskId) || []);
79
+ } else {
80
+ enqueue(successors.get(movedTaskId) || []);
81
+ if (bidirectional) enqueue(predecessors.get(movedTaskId) || []);
82
+ }
83
+
84
+ while (queue.length > 0) {
85
+ const id = queue.shift();
86
+ result.set(id, deltaMs);
87
+ if (upstream_only && !bidirectional) {
88
+ enqueue(predecessors.get(id) || []);
89
+ } else {
90
+ enqueue(successors.get(id) || []);
91
+ if (bidirectional) enqueue(predecessors.get(id) || []);
92
+ }
93
+ }
94
+
95
+ return result;
96
+ }
97
+
98
+ /**
99
+ * Dependency-type-aware shifting.
100
+ * Forward pass (topological order): shift downstream tasks by the minimum needed to resolve conflicts.
101
+ * Backward pass (reverse topological order): pull upstream tasks earlier if needed.
102
+ * Diamond resolution: when multiple predecessors propose a shift, take the maximum.
103
+ */
104
+ function _consume_buffer_shift(movedTaskId, deltaMs, taskById, predecessors, successors, direction) {
105
+ // Kahn's algorithm for topological order
106
+ const in_degree = new Map();
107
+ for (const [id] of taskById) {
108
+ in_degree.set(id, (predecessors.get(id) || []).length);
109
+ }
110
+
111
+ const topo_order = [];
112
+ const queue = [];
113
+ for (const [id, deg] of in_degree) {
114
+ if (deg === 0) queue.push(id);
115
+ }
116
+ while (queue.length > 0) {
117
+ const id = queue.shift();
118
+ topo_order.push(id);
119
+ for (const { id: succId } of successors.get(id) || []) {
120
+ const new_deg = in_degree.get(succId) - 1;
121
+ in_degree.set(succId, new_deg);
122
+ if (new_deg === 0) queue.push(succId);
123
+ }
124
+ }
125
+
126
+ // The moved task's dates are already updated by date_changed() before this
127
+ // function is called, so its effective shift is 0 — no double-counting.
128
+ const shifts = new Map([[movedTaskId, 0]]);
129
+
130
+ // Helpers: effective timestamps accounting for accumulated shifts
131
+ const eff_start = (id) => taskById.get(id)._start.getTime() + (shifts.get(id) || 0);
132
+ const eff_end = (id) => taskById.get(id)._end.getTime() + (shifts.get(id) || 0);
133
+
134
+ // Forward pass: push downstream tasks later when a conflict exists
135
+ if (direction === 'downstream' || direction === 'both') {
136
+ for (const id of topo_order) {
137
+ if (id === movedTaskId) continue;
138
+ let max_shift = 0;
139
+ for (const { id: predId, type } of predecessors.get(id) || []) {
140
+ const needed = _conflict_shift(predId, id, type, eff_start, eff_end);
141
+ if (needed > max_shift) max_shift = needed;
142
+ }
143
+ if (max_shift > 0) {
144
+ shifts.set(id, (shifts.get(id) || 0) + max_shift);
145
+ }
146
+ }
147
+ }
148
+
149
+ // Backward pass: pull upstream tasks earlier when a conflict exists
150
+ if (direction === 'upstream' || direction === 'both') {
151
+ for (let i = topo_order.length - 1; i >= 0; i--) {
152
+ const id = topo_order[i];
153
+ if (id === movedTaskId) continue;
154
+ let max_pull = 0; // most-negative value wins (furthest earlier)
155
+ for (const { id: succId, type } of successors.get(id) || []) {
156
+ const needed = _conflict_shift(id, succId, type, eff_start, eff_end);
157
+ // If a forward conflict exists for this pair, the predecessor must shift earlier
158
+ if (needed > 0) {
159
+ const pull = -needed;
160
+ if (pull < max_pull) max_pull = pull;
161
+ }
162
+ }
163
+ if (max_pull < 0) {
164
+ shifts.set(id, (shifts.get(id) || 0) + max_pull);
165
+ }
166
+ }
167
+ }
168
+
169
+ // Return map without the moved task (caller already applied its shift)
170
+ const result = new Map();
171
+ for (const [id, shift] of shifts) {
172
+ if (id !== movedTaskId && shift !== 0) {
173
+ result.set(id, shift);
174
+ }
175
+ }
176
+ return result;
177
+ }
178
+
179
+ /**
180
+ * Returns the positive millisecond shift needed to resolve a conflict between pred and succ,
181
+ * based on the dependency type. Returns 0 if no conflict.
182
+ */
183
+ function _conflict_shift(predId, succId, type, eff_start, eff_end) {
184
+ const pred_start = eff_start(predId);
185
+ const pred_end = eff_end(predId);
186
+ const succ_start = eff_start(succId);
187
+ const succ_end = eff_end(succId);
188
+
189
+ switch (type) {
190
+ case 'finish-to-start':
191
+ return pred_end > succ_start ? pred_end - succ_start : 0;
192
+ case 'start-to-start':
193
+ return pred_start > succ_start ? pred_start - succ_start : 0;
194
+ case 'finish-to-finish':
195
+ return pred_end > succ_end ? pred_end - succ_end : 0;
196
+ case 'start-to-finish':
197
+ return pred_start > succ_end ? pred_start - succ_end : 0;
198
+ default:
199
+ return pred_end > succ_start ? pred_end - succ_start : 0;
200
+ }
201
+ }