Diff
Modified: trunk/Tools/ChangeLog (291062 => 291063)
--- trunk/Tools/ChangeLog 2022-03-09 20:59:08 UTC (rev 291062)
+++ trunk/Tools/ChangeLog 2022-03-09 21:05:03 UTC (rev 291063)
@@ -1,3 +1,30 @@
+2021-12-07 Zhifei Fang <[email protected]>
+
+ [results.webkit.org]Add selection box for results database.
+ https://bugs.webkit.org/show_bug.cgi?id=233958
+
+ Reviewed by Jonathan Bedard.
+
+ Allow user can drag a selection box to select multiple dots.
+ Allow user to use cmd + click to select multiple dots.
+ Allow user to use shift + click to select a row of dots.
+
+ * Scripts/libraries/resultsdbpy/resultsdbpy/view/static/js/timeline.js:
+ (TimelineFromEndpoint):
+ (TimelineFromEndpoint.prototype.update):
+ (TimelineFromEndpoint.prototype.getTestResultStatus):
+ (TimelineFromEndpoint.prototype.getTestResultUrl):
+ (TimelineFromEndpoint.prototype.render):
+ * Scripts/libraries/resultsdbpy/resultsdbpy/view/static/js/tooltip.js:
+ * Scripts/libraries/resultsdbpy/resultsdbpy/view/static/library/js/components/TimelineComponents.js:
+ (pointRectCollisionDetect): Detact if a point and a rect have a insertion.
+ (rectRectCollisionDetect): Detact if two rects have a insertion.
+ (XScrollableCanvasProvider): Add selection box.
+ (xScrollStreamRenderFactory):
+ (Timeline.CanvasSeriesComponent): Add selection box , cmd + click and shift + click.
+ (prototype.ExpandableSeriesComponent):
+ (prototype.Timeline.CanvasContainer):
+
2022-03-01 Jonathan Bedard <[email protected]>
[EWS] Support concept of 'blocked' pull requests
Modified: trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/controller/api_routes.py (291062 => 291063)
--- trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/controller/api_routes.py 2022-03-09 20:59:08 UTC (rev 291062)
+++ trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/controller/api_routes.py 2022-03-09 21:05:03 UTC (rev 291063)
@@ -20,6 +20,7 @@
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+from crypt import methods
import traceback
from flask import abort, jsonify
@@ -30,12 +31,13 @@
from resultsdbpy.controller.suite_controller import SuiteController
from resultsdbpy.controller.test_controller import TestController
from resultsdbpy.controller.upload_controller import UploadController
+from resultsdbpy.controller.bug_tracker_controller import BugTrackerController
from webkitflaskpy import AuthedBlueprint
from werkzeug.exceptions import HTTPException
class APIRoutes(AuthedBlueprint):
- def __init__(self, model, import_name=__name__, auth_decorator=None):
+ def __init__(self, model, import_name=__name__, auth_decorator=None, bug_tracker_configs=[]):
super(APIRoutes, self).__init__('controller', import_name, url_prefix='/api', auth_decorator=auth_decorator)
self.commit_controller = CommitController(commit_context=model.commit_context)
@@ -48,6 +50,8 @@
self.ci_controller = CIController(ci_context=model.ci_context, upload_context=model.upload_context)
self.archive_controller = ArchiveController(commit_controller=self.commit_controller, archive_context=model.archive_context, upload_context=model.upload_context)
+ self.bug_tracker_controller = BugTrackerController(bug_tracker_configs=bug_tracker_configs, commit_context=model.commit_context)
+
for code in [400, 404, 405]:
self.register_error_handler(code, self.error_response)
self.register_error_handler(500, self.response_500)
@@ -77,6 +81,10 @@
self.add_url_rule('/urls/queue', 'queue-urls', self.ci_controller.urls_for_queue_endpoint, methods=('GET',))
self.add_url_rule('/urls', 'build-urls', self.ci_controller.urls_for_builds_endpoint, methods=('GET',))
+ if bug_tracker_configs:
+ self.add_url_rule('/bug-trackers', 'bug-trackers', self.bug_tracker_controller.list_trackers, methods=('GET',))
+ self.add_url_rule('/bug-trackers/<path:tracker>/create-bug', 'create-bug', self.bug_tracker_controller.create_bug, methods=('PUT',))
+
def error_response(self, error):
response = jsonify(status='error', error=error.name, description=error.description)
response.status = f'error.{error.name}'
Added: trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/controller/bug_tracker_controller.py (0 => 291063)
--- trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/controller/bug_tracker_controller.py (rev 0)
+++ trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/controller/bug_tracker_controller.py 2022-03-09 21:05:03 UTC (rev 291063)
@@ -0,0 +1,85 @@
+# Copyright (C) 2022 Apple Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from pydoc import describe
+from flask import request, abort, jsonify
+from resultsdbpy.controller.commit_controller import HasCommitContext
+
+
+class BugTrackerConfig(object):
+
+ def __init__(self, name):
+ self._name = name
+ self.commit_context = None
+
+ def name(self):
+ return self._name
+
+ def set_commit_context(self, commit_context):
+ self.commit_context = commit_context
+
+ def create_bug(self, **kwargs):
+ raise NotImplementedError
+
+ @classmethod
+ def get_test_result_url(config_params, commit_json, suite, host, protocol='https'):
+ build_params = {}
+ for k, v in config_params.items():
+ build_params[k] = v
+ build_params['suite'] = [suite]
+ build_params['uuid'] = [commit_json['uuid']]
+ build_params['after_time'] = [commit_json['start_time']]
+ build_params['before_time'] = [commit_json['start_time']]
+
+ query = []
+ for k, v in build_params:
+ query.append('{}={}'.format(k, v))
+
+ return '{}://{}/urls/build?{}'.format(protocol, host, '&'.join(query))
+
+
+class BugTrackerController(HasCommitContext):
+
+ def __init__(self, commit_context, bug_tracker_configs=[]):
+ self.bug_tracker_configs = bug_tracker_configs
+ for bug_tracker_config in self.bug_tracker_configs:
+ bug_tracker_config.set_commit_context(commit_context)
+ super(BugTrackerController, self).__init__(commit_context)
+
+ def list_trackers(self):
+ bug_tracker_names = []
+ if self.bug_tracker_configs:
+ for bug_tracker_config in self.bug_tracker_configs:
+ bug_tracker_names.append(bug_tracker_config.name())
+
+ return jsonify(bug_tracker_names)
+
+ def create_bug(self, tracker):
+ content = request.json
+ for bug_tracker_config in self.bug_tracker_configs:
+ if bug_tracker_config.name() == tracker:
+ try:
+ return jsonify(bug_tracker_config.create_bug(content))
+ except ValueError as e:
+ abort(400, description=str(e))
+
+ return abort(404, description='No such bug tracker')
Modified: trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/model/test_context.py (291062 => 291063)
--- trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/model/test_context.py 2022-03-09 20:59:08 UTC (rev 291062)
+++ trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/model/test_context.py 2022-03-09 21:05:03 UTC (rev 291063)
@@ -52,6 +52,9 @@
CRASH, TIMEOUT, IMAGE, AUDIO, TEXT, FAIL, ERROR, WARNING, PASS = STATE_ID_TO_STRING.values()
+ FAILURE_TYPES = [WARNING, ERROR, TIMEOUT, CRASH]
+ STATS_FAILURE_TYPES = ['warning', 'failed', 'timedout', 'crashed']
+
@classmethod
def string_to_state_ids(cls, string):
result = set([elm for elm in [cls.STRING_TO_STATE_ID.get(str) for str in string.split(' ')] if elm is not None])
@@ -81,7 +84,56 @@
for key, value in results.items():
recurse(key, value)
+ @classmethod
+ def get_test_result_status(cls, test_result_json, will_filter_expected=False):
+ failure_type = None
+ failure_number = None
+ if test_result_json.get('stats', None):
+ if test_result_json.get('start_time', None):
+ failure_number = test_result_json['stats']['tests{}failed'.format('_unexpected_' if will_filter_expected else '_')]
+ else:
+ failure_number = test_result_json['stats']['worst_tests{}failed'.format('_unexpected_' if will_filter_expected else '_')]
+ if 'worst_tests_run' in test_result_json and test_result_json['stats']['worst_tests_run'] <= 1:
+ failure_number = None
+ for type in Expectations.STATS_FAILURE_TYPES:
+ if test_result_json['stats'].get('tests{}{}'.format('_unexpected_' if will_filter_expected else '_', type.lower()), 0) > 0:
+ failure_type = type
+ else:
+ resultId = Expectations.string_to_state_ids(test_result_json['actual'])
+ if will_filter_expected:
+ resultId = Expectations.string_to_state_ids(cls.unexpected_results(test_result_json['actual'], test_result_json['expected']))
+ for type in Expectations.FAILURE_TYPES:
+ if Expectations.string_to_state_ids(type) >= resultId:
+ failure_type = type
+ return failure_type, failure_number
+
+ @classmethod
+ def unexpected_results(cls, results, expectations):
+ """
+ This function is a python translation for expectations.js Expectation.unexpectedResults
+ """
+ r = results.split('.')
+ for expectation in expectations.split(' '):
+ try:
+ i = r.index(expectation)
+ r = r[:i] + r[i + 1:]
+ except ValueError:
+ pass
+ if expectation == cls.FAIL:
+ for expectation in [cls.TEXT, cls.AUDIO, cls.IMAGE]:
+ try:
+ i = r.index(expectation)
+ r = r[:i] + r[i + 1:]
+ except ValueError:
+ pass
+ result = cls.PASS
+ for candidate in r:
+ if Expectations.string_to_state_ids(candidate) < Expectations.string_to_state_ids(result):
+ result = candidate
+ return result
+
+
class TestContext(UploadCallbackContext):
DEFAULT_LIMIT = 100
Modified: trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/view/static/css/tooltip.css (291062 => 291063)
--- trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/view/static/css/tooltip.css 2022-03-09 20:59:08 UTC (rev 291062)
+++ trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/view/static/css/tooltip.css 2022-03-09 21:05:03 UTC (rev 291063)
@@ -23,31 +23,34 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-.tooltip {
+ .tooltip {
z-index: var(--topZIndex);
position: absolute;
- opacity: 80%;
- width: 0;
- height: 0;
- border-style: solid;
- border-width: 15px;
- -webkit-background-clip: padding-box;
- background-clip: padding-box;
+ width: var(--largeSize);
+ height: var(--largeSize);
+ transform-origin: center center;
+ -webkit-backdrop-filter: blur(10px) brightness(88%);
+ backdrop-filter: blur(10px) brightness(88%);
+ color: var(--inverseColor);
}
.tooltip.arrow-up {
- border-color: transparent transparent #cccd transparent;
+ transform: rotate(225deg);
+ clip-path: polygon(100% 0, 100% 100%, 0 100%);
}
.tooltip.arrow-down {
- border-color: #cccd transparent transparent transparent;
+ transform: rotate(45deg);
+ clip-path: polygon(100% 0, 100% 100%, 0 100%);
}
.tooltip.arrow-left {
- border-color: transparent transparent transparent #cccd;
+ transform: rotate(45deg);
+ clip-path: polygon(0 0, 100% 100%, 0 100%);
}
.tooltip.arrow-right {
- border-color: transparent #cccd transparent transparent;
+ transform: rotate(225deg);
+ clip-path: polygon(0 0, 0 100%, 100% 100%);
}
.tooltip-content {
Modified: trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/view/static/js/timeline.js (291062 => 291063)
--- trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/view/static/js/timeline.js 2022-03-09 20:59:08 UTC (rev 291062)
+++ trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/view/static/js/timeline.js 2022-03-09 21:05:03 UTC (rev 291063)
@@ -353,6 +353,7 @@
this.configurations = Configuration.fromQuery();
this.results = {};
+ this.selectedDots = new Map();
// Suite and test can often be implied by the endpoint, but doing so is more confusing then helpful
this.suite = suite;
@@ -379,7 +380,6 @@
element.innerHTML = this.placeholder();
}
});
-
this.commit_callback = () => {
self.update();
};
@@ -393,6 +393,8 @@
});
}
update() {
+ if (this.selectedDotsButtonGroupRef)
+ this.selectedDotsButtonGroupRef.setState({show: false});
const params = queryToParams(document.URL.split('?')[1]);
const commits = commitsForResults(this.results, params.limit ? parseInt(params.limit[params.limit.length - 1]) : DEFAULT_LIMIT, this.allCommits);
const scale = scaleForCommits(commits);
@@ -503,9 +505,113 @@
}
});
- return `<div class="content" ref="${this.ref}"></div>`;
+ return `
+ <div style="position:relative">
+ <div class="content" ref="${this.ref}"></div>
+ </div>`;
}
+ getTestResultStatus(data, willFilterExpected=false) {
+ let failureType = null;
+ let failureNumber = null;
+ if (data.stats) {
+ if (data.start_time)
+ failureNumber = data.stats[`tests${willFilterExpected ? '_unexpected_' : '_'}failed`];
+ else
+ failureNumber = data.stats[`worst_tests${willFilterExpected ? '_unexpected_' : '_'}failed`];
+ if (data.stats.worst_tests_run <= 1)
+ failureNumber = null;
+
+ Expectations.failureTypes.forEach(type => {
+ if (data.stats[`tests${willFilterExpected ? '_unexpected_' : '_'}${type}`] > 0) {
+ failureType = type;
+ }
+ });
+ } else {
+ let resultId = Expectations.stringToStateId(data.actual);
+ if (willFilterExpected)
+ resultId = Expectations.stringToStateId(Expectations.unexpectedResults(data.actual, data.expected));
+ Expectations.failureTypes.forEach(type => {
+ if (Expectations.stringToStateId(Expectations.failureTypeMap[type]) >= resultId) {
+ failureType = type;
+ }
+ });
+ }
+ return {failureType, failureNumber};
+ }
+
+ getTestResultUrl(config, data) {
+ const buildParams = config.toParams();
+ buildParams['suite'] = [this.suite];
+ buildParams['uuid'] = [data.uuid];
+ buildParams['after_time'] = [data.start_time];
+ buildParams['before_time'] = [data.start_time];
+ return `${window.location.protocol}//${window.location.host}/urls/build?${paramsToQuery(buildParams)}`;
+ }
+
+ _renderSelectedDotsButtonGroup(element) {
+ DOM.inject(element, this.bugTrackers.map(bugTracker => {
+ const buttonText = `${bugTracker}`;
+ const buttonRef = REF.createRef({
+ state: {
+ loading: false
+ },
+ onStateUpdate: (element, stateDiff) => {
+ if (stateDiff.loading)
+ element.innerText = 'Waiting...';
+ else
+ element.innerText = buttonText;
+ }
+ });
+
+ buttonRef.fromEvent('click').action(e => {
+ const requestPayload = {
+ selectedRows: [],
+ willFilterExpected: InvestigateDrawer.willFilterExpected,
+ repositories: this.repositories,
+ suite: this.suite,
+ test: this.test
+ };
+ Array.from(this.selectedDots.keys()).forEach(config => {
+ const dots = this.selectedDots.get(config);
+ requestPayload.selectedRows.push({
+ config,
+ results: dots
+ });
+ });
+ buttonRef.setState({loading: true});
+ fetch(`api/bug-trackers/${bugTracker}/create-bug`, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(requestPayload)
+ }).then(res => {
+ if (res.ok)
+ return res.json()
+ return res.json().then((data) => {
+ throw new Error(data.description);
+ });
+ }).then(data ="" {
+ const bugLinkElement = document.createElement('a');
+ bugLinkElement.setAttribute('href', data['url']);
+ bugLinkElement.click();
+ }).catch(e => {
+ alert(e);
+ }).finally(() => {
+ buttonRef.setState({loading: false});
+ });
+
+ });
+
+ return `<div>
+ <button class="button tiny" style="position:absolute; background: var(--purple); color: var(--white)" ref="${buttonRef}">
+ ${buttonText}
+ </button>
+ </div>`;
+ }).join(''));
+ }
+
render(limit) {
const branch = queryToParams(document.URL.split('?')[1]).branch;
const self = this;
@@ -512,6 +618,59 @@
const commits = commitsForResults(this.results, limit, this.allCommits);
const scale = scaleForCommits(commits);
+ this.selectedDotsButtonGroupRef = REF.createRef({
+ state: {
+ show: false,
+ top: 0,
+ left: 0,
+ },
+ onElementMount: (element) => {
+ if (this.bugTrackers)
+ this._renderSelectedDotsButtonGroup(element);
+ fetch('api/bug-trackers').then(res => res.json()).then(bugTrackers => {
+ this.bugTrackers = bugTrackers;
+ this._renderSelectedDotsButtonGroup(element);
+ })
+ },
+ onStateUpdate: (element, stateDiff) => {
+ if ('show' in stateDiff) {
+ if (stateDiff.show)
+ element.style.display = 'block';
+ else
+ element.style.display = 'none';
+ }
+ if ('top' in stateDiff)
+ element.style.top = `${stateDiff.top}px`;
+ if ('left' in stateDiff) {
+ const rect = element.getBoundingClientRect();
+ element.style.left = `${stateDiff.left - rect.width}px`;
+ }
+ }
+ });
+
+ this.radarButtonRef = REF.createRef({
+ state: {
+ show: false,
+ top: 0,
+ left: 0,
+ loading: false,
+ },
+ onStateUpdate: (element, stateDiff) => {
+ if ('show' in stateDiff) {
+ if (stateDiff.show)
+ element.style.display = 'block';
+ else
+ element.style.display = 'none';
+ }
+ if ('top' in stateDiff)
+ element.style.top = `${stateDiff.top}px`;
+ if ('left' in stateDiff) {
+ const rect = element.getBoundingClientRect();
+ element.style.left = `${stateDiff.left - rect.width}px`;
+ }
+ }
+ });
+
const colorMap = Expectations.colorMap();
this.updates = [];
const options = {
@@ -736,6 +895,12 @@
onDotClick: onDotClickFactory(config),
onDotEnter: onDotEnterFactory(config),
onDotLeave: onDotLeave,
+ onDotsSelected: dots => {
+ if (dots.length)
+ this.selectedDots.set(config, dots);
+ else
+ this.selectedDots.delete(config);
+ },
exporter: exporterFactory(resultsForConfig),
}));
@@ -752,6 +917,12 @@
onDotClick: onDotClickFactory(sdkConfig),
onDotEnter: onDotEnterFactory(sdkConfig),
onDotLeave: onDotLeave,
+ onDotsSelected: dots => {
+ if (dots.length)
+ this.selectedDots.set(sdkConfig, dots);
+ else
+ this.selectedDots.delete(sdkConfig);
+ },
exporter: exporterFactory(resultsByKey[sdkConfig.toKey()]),
})));
});
@@ -779,6 +950,12 @@
onDotClick: onDotClickFactory(configuration),
onDotEnter: onDotEnterFactory(configuration),
onDotLeave: onDotLeave,
+ onDotsSelected: dots => {
+ if (dots.length)
+ this.selectedDots.set(configuration, dots);
+ else
+ this.selectedDots.delete(configuration);
+ },
exporter: exporterFactory(allResults),
})),
{expanded: this.configurations.length <= 1},
@@ -816,7 +993,20 @@
};
self.notifyRerender = notifyRerender;
}));
- return Timeline.CanvasContainer(composer, ...children);
+ return Timeline.CanvasContainer({
+ customizedLayer: `<div class="row" style="position:absolute" ref="${this.selectedDotsButtonGroupRef}"></div>`,
+ onSelecting: (e) => {
+ this.selectedDotsButtonGroupRef.setState({show: false});
+ },
+ onSelect: (dots, selectedDotRect, seriesRect, e) => {
+ // this api will called with selected dots for each series once, and compose the selectedDotRect during the call
+ this.selectedDotsButtonGroupRef.setState({show: true, top: selectedDotRect.bottom, left: selectedDotRect.right});
+ },
+ onSelectionScroll: (dots, selectedDotRect) => {
+ // this api will called with selected dots for each series once, and compose the selectedDotRect during the call
+ this.selectedDotsButtonGroupRef.setState({top: selectedDotRect.bottom, left: selectedDotRect.right});
+ },
+ }, composer, ...children);
}
}
@@ -936,3 +1126,4 @@
}
export {Legend, TimelineFromEndpoint, Expectations};
+
Modified: trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/view/static/js/tooltip.js (291062 => 291063)
--- trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/view/static/js/tooltip.js 2022-03-09 20:59:08 UTC (rev 291062)
+++ trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/view/static/js/tooltip.js 2022-03-09 21:05:03 UTC (rev 291063)
@@ -69,6 +69,7 @@
const lowerPoint = stateDiff.points.length > 1 && stateDiff.points[1].y > stateDiff.points[0].y ? stateDiff.points[1] : stateDiff.points[0];
const scrollDelta = document.documentElement.scrollTop || document.body.scrollTop;
const bounds = element.getBoundingClientRect();
+ const computedStyle = getComputedStyle(element);
let direction = 'down';
let point = upperPoint;
@@ -79,11 +80,11 @@
const rightPoint = stateDiff.points.length > 1 && stateDiff.points[1].x > stateDiff.points[0].x ? stateDiff.points[1] : stateDiff.points[0];
direction = 'left';
- let tipX = leftPoint.x - 12 - bounds.width;
+ let tipX = leftPoint.x - bounds.width;
point = rightPoint;
if (tipX < 0 || tipX + bounds.width + (rightPoint.x - leftPoint.x) / 2 < stateDiff.viewport.x + stateDiff.viewport.width / 2) {
direction = 'right';
- tipX = rightPoint.x + 16;
+ tipX = rightPoint.x;
point = rightPoint;
}
element.style.left = `${tipX}px`;
@@ -96,11 +97,11 @@
element.style.top = `${tipY}px`;
} else {
// Make an effort to place the tooltip in the center of the viewport.
- let tipY = upperPoint.y - 8 - bounds.height;
+ let tipY = upperPoint.y - bounds.height;
point = upperPoint;
if (tipY < scrollDelta || tipY + bounds.height + (lowerPoint.y - upperPoint.y) / 2 < scrollDelta + stateDiff.viewport.y + stateDiff.viewport.height / 2) {
direction = 'up';
- tipY = lowerPoint.y + 16;
+ tipY = lowerPoint.y + parseFloat(computedStyle.getPropertyValue('--smallSize'));
point = lowerPoint;
}
element.style.top = `${tipY}px`;
@@ -144,19 +145,20 @@
}
element.classList = [`tooltip arrow-${stateDiff.direction}`];
-
+ const computedStyle = getComputedStyle(element);
+ const {width, height} = computedStyle;
if (stateDiff.direction == 'down') {
- element.style.left = `${stateDiff.location.x - 15}px`;
- element.style.top = `${stateDiff.location.y - 8}px`;
+ element.style.left = `calc(${stateDiff.location.x}px - ${width} / 2)`;
+ element.style.top = `calc(${stateDiff.location.y}px - ${height} / 2)`;
} else if (stateDiff.direction == 'left') {
- element.style.left = `${stateDiff.location.x - 30}px`;
- element.style.top = `${stateDiff.location.y - 15}px`;
+ element.style.left = `calc(${stateDiff.location.y}px - ${width} / 2)`;
+ element.style.top = `calc(${stateDiff.location.x}px - ${height} / 2)`;
} else if (stateDiff.direction == 'right') {
- element.style.left = `${stateDiff.location.x - 13}px`;
- element.style.top = `${stateDiff.location.y - 15}px`;
+ element.style.left = `calc(${stateDiff.location.y}px - ${width} / 2)`;
+ element.style.top = `calc(${stateDiff.location.x}px - ${height} / 2)`;
} else {
- element.style.left = `${stateDiff.location.x - 15}px`;
- element.style.top = `${stateDiff.location.y - 13}px`;
+ element.style.left = `calc(${stateDiff.location.x}px - ${width} / 2)`;
+ element.style.top = `calc(${stateDiff.location.y}px - ${height} / 2 + ${computedStyle.getPropertyValue('--smallSize')})`;
}
element.style.display = null;
},
Modified: trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/view/static/library/js/components/TimelineComponents.js (291062 => 291063)
--- trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/view/static/library/js/components/TimelineComponents.js 2022-03-09 20:59:08 UTC (rev 291062)
+++ trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/view/static/library/js/components/TimelineComponents.js 2022-03-09 21:05:03 UTC (rev 291063)
@@ -33,11 +33,22 @@
}
function pointRectCollisionDetect(point, rect) {
- const diffX = point.x - rect.topLeftX;
- const diffY = point.y - rect.topLeftY;
- return diffX <= rect.width && diffY <= rect.height && diffX >= 0 && diffY >= 0;
+ const x1 = rect.left;
+ const x2 = rect.left + rect.width;
+ const y1 = rect.top;
+ const y2 = rect.top + rect.height;
+ return (point.x > x1 && point.x < x2) && (point.y > y1 && point.y < y2);
}
+function rectRectCollisionDetect(rectA, rectB) {
+ return !(
+ (rectA.left + rectA.width) < rectB.left ||
+ rectA.left > (rectB.left + rectB.width) ||
+ rectA.top > (rectB.top + rectB.height) ||
+ (rectA.top + rectA.height) < rectB.top
+ );
+}
+
function pointPolygonCollisionDetect(point, polygon) {
let res = false;
for (let i = 0, j = 1; i < polygon.length; i++, j = i + 1) {
@@ -103,7 +114,22 @@
context.scale(dpr, dpr);
}
-function XScrollableCanvasProvider(exporter, ...childrenFunctions) {
+function XScrollableCanvasProvider(props, exporter, ...childrenFunctions) {
+ let drag = false;
+ let initPos = null;
+ const _onSelect_ = 'onSelect' in props ? props.onSelect : null;
+ const _onSelecting_ = 'onSelecting' in props ? props.onSelecting: null;
+ const _onSelectionScroll_ = 'onSelectionScroll' in props ? props.onSelectionScroll: null;
+ const _onScroll_ = 'onScroll' in props ? props.onScroll : null;
+ const customizedLayer = 'customizedLayer' in props ? props.customizedLayer : '';
+ const timeLineMouseUpEventStream = new EventStream();
+ const selectionBoxChangeEventStream = new EventStream();
+ const selectionBoxDoneEventStream = new EventStream();
+ const selectingEventStream = new EventStream();
+ const clickSelectionDoneEventStream = new EventStream();
+ const selectedDotsScrollEventStream = new EventStream();
+ const resizeEventStream = new EventStream();
+
const containerRef = REF.createRef({
state: {width: 0},
onStateUpdate: (element, stateDiff, state) => {
@@ -111,9 +137,16 @@
element.style.width = `${stateDiff.width}px`;
},
});
- const scrollRef = REF.createRef({});
+ const containerMouseDownEventStream = containerRef.fromEvent('mousedown');
+ const containerMouseMoveEventStream = containerRef.fromEvent('mousemove');
+ const scrollRef = REF.createRef({
+ onElementMount: (element, stateDiff) => {
+ element.parentElement.addEventListener('mouseup', e => {
+ timeLineMouseUpEventStream.add(e);
+ });
+ }
+ });
const scrollEventStream = scrollRef.fromEvent('scroll');
- const resizeEventStream = new EventStream();
window.addEventListener('resize', () => {
presenterRef.setState({resize:true});
});
@@ -138,11 +171,132 @@
layoutSizeMayChange.action(() => {
presenterRef.setState({resize:true});
});
+
+ // Selection box
+ const selectionBoxRef = REF.createRef({
+ state: {x: 0, y: 0, width: 0, height: 0, show: false},
+ onStateUpdate: (element, stateDiff) => {
+ if ('x' in stateDiff)
+ element.style.left = `${stateDiff.x}px`;
+ if ('y' in stateDiff)
+ element.style.top = `${stateDiff.y}px`;
+ if ('width' in stateDiff)
+ element.style.width = `${stateDiff.width}px`;
+ if ('height' in stateDiff)
+ element.style.height = `${stateDiff.height}px`;
+ if ('show' in stateDiff)
+ if (stateDiff.show)
+ element.style.display = 'block';
+ else
+ element.style.display = 'none';
+ }
+ });
+ const selectedDotRect = {
+ left: Number.MAX_VALUE,
+ top: Number.MAX_VALUE,
+ right: 0,
+ bottom: 0,
+ width: 0,
+ height: 0
+ };
+
+ const updateSelectedDotsRect = (dots, seriesRect) => {
+ const contentRect = scrollRef.element.getBoundingClientRect();
+ const dotLeft = dots[0]._dotCenter.x - dots[0]._dotRadius + dots[0]._cachedScrollLeft * getDevicePixelRatio();
+ const dotRight = dots[dots.length - 1]._dotCenter.x + dots[dots.length - 1]._dotRadius + dots[dots.length - 1]._cachedScrollLeft * getDevicePixelRatio();
+ const dotTop = seriesRect.top - contentRect.top;
+ const dotBottom = seriesRect.bottom - contentRect.top;
+ if (dotLeft < selectedDotRect.left)
+ selectedDotRect.left = dotLeft;
+ if (dotTop < selectedDotRect.top)
+ selectedDotRect.top = dotTop;
+ if (dotRight > selectedDotRect.right)
+ selectedDotRect.right = dotRight;
+ if (dotBottom > selectedDotRect.bottom)
+ selectedDotRect.bottom = dotBottom;
+ selectedDotRect.width = selectedDotRect.right - selectedDotRect.left;
+ selectedDotRect.height = selectedDotRect.bottom - selectedDotRect.top;
+ };
+
+ scrollEventStream.action(e => {
+ selectedDotRect.left = Number.MAX_VALUE;
+ selectedDotRect.right = 0;
+ selectedDotRect.top = Number.MAX_VALUE;
+ selectedDotRect.bottom = 0;
+ selectedDotRect.width = 0;
+ selectedDotRect.height = 0;
+ if (onScroll)
+ onScroll(e);
+ });
+
+ selectingEventStream.action(() => {
+ if (onSelecting)
+ onSelecting();
+ });
+
+ selectedDotsScrollEventStream.action((dots, seriesRect) => {
+ if (!dots || !dots.length)
+ return;
+ updateSelectedDotsRect(dots, seriesRect);
+ if (onSelectionScroll)
+ onSelectionScroll(dots, selectedDotRect, seriesRect);
+ });
+
+ containerMouseDownEventStream.action(e => {
+ if (e.buttons != 1)
+ return;
+ drag = true;
+ initPos = getMousePosInCanvas(e, containerRef.element);
+ selectedDotRect.left = Number.MAX_VALUE;
+ selectedDotRect.right = 0;
+ selectedDotRect.top = Number.MAX_VALUE;
+ selectedDotRect.bottom = 0;
+ selectedDotRect.width = 0;
+ selectedDotRect.height = 0;
+ });
+
+ containerMouseMoveEventStream.action(e => {
+ if (!drag)
+ return;
+ selectingEventStream.add(e);
+ });
+
+ const _onSelectionDoneCallback_ = (dots, seriesRect, e) => {
+ if (!dots || !dots.length)
+ return;
+ updateSelectedDotsRect(dots, seriesRect);
+ if (onSelect)
+ onSelect(dots, selectedDotRect, seriesRect, e);
+ };
+ timeLineMouseUpEventStream.action(e => {
+ drag = false;
+ selectionBoxRef.setState({show: false});
+ if (onSelect)
+ selectionBoxDoneEventStream.add(onSelectionDoneCallback, e);
+ });
+ clickSelectionDoneEventStream.action(onSelectionDoneCallback);
+ window.addEventListener('mouseup', e => {
+ drag = false;
+ selectionBoxRef.setState({show: false});
+ });
+ window.addEventListener('mousemove', e => {
+ if (!drag)
+ return;
+ const cursorPos = getMousePosInCanvas(e, containerRef.element);
+ const width = Math.abs(cursorPos.x - initPos.x);
+ const height = Math.abs(cursorPos.y - initPos.y);
+ let x = initPos.x < cursorPos.x ? initPos.x : cursorPos.x;
+ let y = initPos.y < cursorPos.y ? initPos.y : cursorPos.y;
+ const containerBoundingRect = containerRef.element.getBoundingClientRect();
+ // notify with absolute pos
+ selectionBoxChangeEventStream.add({left: x + containerBoundingRect.left, top: y + containerBoundingRect.top, width, height});
+ selectionBoxRef.setState({x, y, width, height, show: true});
+ });
// Provide parent functions/event to children to use
return `<div class="content" ref="${scrollRef}">
<div ref="${containerRef}" style="position: relative">
- <div ref="${presenterRef}" style="position: -webkit-sticky; position:sticky; top:0; left: 0">${
+ <div ref="${presenterRef}" style="position: -webkit-sticky; position:sticky; top:0; left: 0; user-select: none; -webkit-user-select: none;">${
ListProvider((updateChildrenFunctions) => {
if (exporter) {
exporter(
@@ -168,8 +322,15 @@
}
);
}
- }, [resizeContainerWidth, scrollEventStream, resizeEventStream, layoutSizeMayChange], ...childrenFunctions)
+ }, [
+ resizeContainerWidth, scrollEventStream, resizeEventStream,
+ layoutSizeMayChange, selectionBoxChangeEventStream,
+ selectionBoxDoneEventStream, selectedDotsScrollEventStream,
+ selectingEventStream, clickSelectionDoneEventStream
+ ], ...childrenFunctions)
}</div>
+ ${customizedLayer}
+ <div ref="${selectionBoxRef}" style="position:absolute; width: 0; height: 0; border: 1px solid var(--inverseColor)"></div>
</div>
</div>`;
}
@@ -202,7 +363,7 @@
}
}
-function xScrollStreamRenderFactory(height) {
+function xScrollStreamRenderFactory(height, callback=null) {
return (redraw, element, stateDiff, state) => {
const width = typeof stateDiff.width === 'number' ? stateDiff.width : state.width;
if (width <= 0)
@@ -221,6 +382,8 @@
}
element.getContext("2d", {alpha: false}).clearRect(startX, 0, renderWidth, element.logicHeight);
redraw(startX, renderWidth, element, stateDiff, state);
+ if (callback)
+ callback(element, stateDiff, state);
});
}
}
@@ -243,6 +406,7 @@
// Get configuration
// Default order is left is biggest
+ const selectedOutLineSize = 3;
const reversed = typeof option.reversed === "boolean" ? option.reversed : false;
const getScale = typeof option.getScaleFunc === "function" ? option.getScaleFunc : (a) => a;
const comp = typeof option.compareFunc === "function" ? option.compareFunc : (a, b) => a - b;
@@ -249,8 +413,9 @@
const _onDotClick_ = typeof option._onDotClick_ === "function" ? option.onDotClick : null;
const _onDotEnter_ = typeof option._onDotEnter_ === "function" ? option.onDotEnter : null;
const _onDotLeave_ = typeof option._onDotLeave_ === "function" ? option.onDotLeave : null;
+ const _onDotsSelected_ = typeof option._onDotsSelected_ === "function" ? option.onDotsSelected : null;
const tagHeight = defaultFontSize;
- const height = option.height ? option.height : 2 * radius + tagHeight;
+ const height = option.height ? option.height : 2 * radius + tagHeight + selectedOutLineSize;
const colorBatchRender = new ColorBatchRender();
let drawLabelsSeqs = [];
@@ -349,15 +514,16 @@
const dotWidth = 2 * (radius + dotMargin);
const padding = 100 * dotWidth / getDevicePixelRatio();
- const xScrollStreamRender = xScrollStreamRenderFactory(height);
const redraw = (startX, renderWidth, element, stateDiff, state) => {
const scrollLeft = typeof stateDiff.scrollLeft === 'number' ? stateDiff.scrollLeft : state.scrollLeft;
const scales = stateDiff.scales ? stateDiff.scales : state.scales;
const dots = stateDiff.dots ? stateDiff.dots : state.dots;
+ if ('dots' in stateDiff)
+ dots.forEach((dot, index) => dot._index = index);
// This color maybe change when switch dark/light mode
const defaultLineColor = getComputedStyle(document.body).getPropertyValue('--borderColorInlineElement');
-
+ const defaultSelectedBackgroundColor = getComputedStyle(document.body).getPropertyValue('--blue');
const context = element.getContext("2d", { alpha: false });
// Clear pervious batchRender
colorBatchRender.clear();
@@ -369,9 +535,18 @@
context.strokeStyle = color;
context.stroke();
});
+
+ colorBatchRender.lazyCreateColorSeqs(defaultSelectedBackgroundColor, (context) => {
+ context.beginPath();
+ }, (context, color) => {
+ context.lineWidth = selectedOutLineSize;
+ context.strokeStyle = color;
+ context.stroke();
+ });
+
colorBatchRender.addSeq(defaultLineColor, (context) => {
- context.moveTo(startX, radius);
- context.lineTo(startX + renderWidth, radius);
+ context.moveTo(startX, radius + selectedOutLineSize);
+ context.lineTo(startX + renderWidth, radius + selectedOutLineSize);
});
if (!scales || !dots || !scales.length || !dots.length) {
colorBatchRender.batchRender(context);
@@ -406,18 +581,31 @@
for (let i = startScalesIndex; i <= endScalesIndex; i++) {
let x = i * dotWidth - scrollLeft;
if (currentDotIndex < dots.length && comp(scales[i], getScale(dots[currentDotIndex])) === 0) {
- render(dots[currentDotIndex], context, x, 0);
+ if(dots[currentDotIndex]._selected) {
+ colorBatchRender.addSeq(defaultSelectedBackgroundColor, (context, color) => {
+ context.arc(x + dotMargin + radius, radius + selectedOutLineSize, radius, 0, 2 * Math.PI);
+ });
+ }
+ render(dots[currentDotIndex], context, x, selectedOutLineSize);
dots[currentDotIndex]._dotCenter = {x: x + dotMargin + radius, y: radius};
+ dots[currentDotIndex]._dotRadius = radius;
dots[currentDotIndex]._cachedScrollLeft = scrollLeft;
inCacheDots.push(dots[currentDotIndex]);
currentDotIndex += 1;
} else
- render(null, context, x, 0);
+ render(null, context, x, selectedOutLineSize);
}
colorBatchRender.batchRender(context);
};
- return ListProviderReceiver((updateContainerWidth, onContainerScroll, onResize) => {
+ return ListProviderReceiver((
+ updateContainerWidth, onContainerScroll, onResize,
+ layoutSizeMayChange, onSelectionBoxChange, onSelectionBoxDone,
+ onSelectionScroll, onSelecting, onClickSelectionDone
+ ) => {
+ let selectedDots = [];
+ let cmdKeyDown = null;
+ let shiftKeyDown = null;
const mouseMove = (e) => {
let dots = getMouseEventTirggerDots(e, canvasRef.state.scrollLeft, canvasRef.element);
if (dots.length) {
@@ -435,6 +623,10 @@
canvasRef.element.style.cursor = "default";
}
}
+ const unselectAllDots = () => {
+ selectedDots = [];
+ canvasRef.state.dots.forEach(dot => dot._selected = false);
+ };
const _onScrollAction_ = (e) => {
canvasRef.setState({scrollLeft: e.target.scrollLeft / getDevicePixelRatio()});
mouseMove(e);
@@ -443,6 +635,59 @@
canvasRef.setState({width: width});
};
+ const xScrollStreamRender = xScrollStreamRenderFactory(height, (element, stateDiff, state) => {
+ if (selectedDots && selectedDots.length && 'scrollLeft' in stateDiff) {
+ const canvasBoundingRect = canvasRef.element.getBoundingClientRect();
+ onSelectionScroll.add(selectedDots, canvasBoundingRect);
+ }
+ });
+
+ const _onSelectionBoxChangeAction_ = (selectionBoxRect) => {
+ const canvasBoundingRect = canvasRef.element.getBoundingClientRect();
+ if (!rectRectCollisionDetect(selectionBoxRect, canvasBoundingRect)) {
+ unselectAllDots();
+ canvasRef.setState({hasDotSelected: true});
+ if (onDotsSelected)
+ onDotsSelected(selectedDots);
+ return;
+ }
+ canvasRef.state.dots.forEach(dot => dot._selected = false);
+ selectedDots = inCacheDots.filter(dot => {
+ // convert to absoulte pos
+ const dotCenter = {
+ x: dot._dotCenter.x + canvasBoundingRect.left,
+ y: dot._dotCenter.y + canvasBoundingRect.top,
+ };
+ return pointRectCollisionDetect(dotCenter, selectionBoxRect);
+ });
+ selectedDots.forEach(dot => {
+ dot._selected = true;
+ });
+ if (onDotsSelected)
+ onDotsSelected(selectedDots);
+ canvasRef.setState({hasDotSelected: true});
+ };
+
+ const _onSelectionBoxDoneAction_ = (onSelectionDoneCallback, e) => {
+ if (selectedDots && selectedDots.length) {
+ const canvasBoundingRect = canvasRef.element.getBoundingClientRect();
+ onSelectionDoneCallback(selectedDots, canvasBoundingRect, e);
+ }
+ }
+
+ const globalKeyDownAction = (e) => {
+ if (e.key === 'Meta')
+ cmdKeyDown = true;
+ else if (e.key === 'Shift')
+ shiftKeyDown = true;
+ };
+ const globalKeyUpAction =(e) => {
+ if (e.key === 'Meta')
+ cmdKeyDown = false;
+ else if (e.key === 'Shift')
+ shiftKeyDown = false;
+ };
+
const canvasRef = REF.createRef({
state: {
dots: initDots,
@@ -450,17 +695,50 @@
scrollLeft: 0,
width: 0,
onScreen: false,
+ hasDotSelected: false,
},
onElementMount: (element) => {
setupCanvasHeightWithDpr(element, height);
setupCanvasContextScale(element);
- if (onDotClick) {
- element.addEventListener('click', (e) => {
- let dots = getMouseEventTirggerDots(e, canvasRef.state.scrollLeft, element);
- if (dots.length)
- onDotClick(dots[0], e);
- });
- }
+ window.addEventListener('keydown',globalKeyDownAction);
+ window.addEventListener('keyup', globalKeyUpAction);
+ element.addEventListener('click', (e) => {
+ let dots = getMouseEventTirggerDots(e, canvasRef.state.scrollLeft, element);
+ if (!dots.length)
+ return;
+ if (onDotClick)
+ onDotClick(dots[0], e);
+ if (!shiftKeyDown && cmdKeyDown) {
+ selectedDots.push(dots[0]);
+ dots[0]._selected = true;
+ }
+ else if (!cmdKeyDown && shiftKeyDown) {
+ let startDotIndex = c
+ let endDotIndex = dots[0]._index;
+ if (selectedDots[0]._index < dost[0]._index) {
+ if (selectedDots[selectedDots.length - 1]._index > dots[0]._index)
+ startDotIndex = selectedDots[0]._index;
+ else
+ startDotIndex = selectedDots[selectedDots.length - 1]._index;
+ endDotIndex = dots[0]._index;
+ } else if (selectedDots[0]._index > dost[0]._index) {
+ startDotIndex = dost[0]._index;
+ endDotIndex = selectedDots[0]._index;
+ }
+ unselectAllDots();
+ for(let i = startDotIndex; i <= endDotIndex; i++) {
+ canvasRef.state.dots[i]._selected = true;
+ selectedDots.push(canvasRef.state.dots[i]);
+ }
+ }
+ if (shiftKeyDown || cmdKeyDown) {
+ onSelecting.add(e);
+ onClickSelectionDone.add(selectedDots, canvasRef.element.getBoundingClientRect(), e);
+ if (onDotsSelected)
+ onDotsSelected(selectedDots);
+ canvasRef.setState({hasDotSelected: true});
+ }
+ });
if (onDotClick || onDotEnter || onDotLeave)
element.addEventListener('mousemove', mouseMove);
@@ -474,6 +752,10 @@
onElementUnmount: (element) => {
onContainerScroll.stopAction(onScrollAction);
onResize.stopAction(onResizeAction);
+ onContainerScroll.stopAction(onScrollAction);
+ onSelectionBoxDone.stopAction(onSelectionBoxDoneAction);
+ window.removeEventListener('keydown',globalKeyDownAction);
+ window.removeEventListener('keyup', globalKeyUpAction);
// Clean the canvas, free its memory
element.width = 0;
element.height = 0;
@@ -481,7 +763,7 @@
onStateUpdate: (element, stateDiff, state) => {
if (!state.onScreen && !stateDiff.onScreen)
return;
- if (stateDiff.scales || stateDiff.dots || typeof stateDiff.scrollLeft === 'number' || typeof stateDiff.width === 'number' || stateDiff.onScreen) {
+ if (stateDiff.scales || stateDiff.dots || typeof stateDiff.scrollLeft === 'number' || typeof stateDiff.width === 'number' || stateDiff.onScreen || stateDiff.hasDotSelected) {
if (stateDiff.scales)
stateDiff.scales = stateDiff.scales.map(x => x);
if (stateDiff.dots)
@@ -503,7 +785,9 @@
option.exporter(updateData);
onContainerScroll.action(onScrollAction);
onResize.action(onResizeAction);
- return `<div class="series">
+ onSelectionBoxChange.action(onSelectionBoxChangeAction);
+ onSelectionBoxDone.action(onSelectionBoxDoneAction);
+ return `<div class="series" style="user-select: none; -webkit-user-select: none;">
<canvas ref="${canvasRef}" width="0" height="0">
</div>`;
});
@@ -529,12 +813,23 @@
});
if (exporter)
exporter((expanded) => ref.setState({expanded: expanded}));
- return ListProviderReceiver((updateContainerWidth, onContainerScroll, onResize, layoutSizeMayChange) => {
+ return ListProviderReceiver((
+ updateContainerWidth, onContainerScroll, onResize,
+ layoutSizeMayChange, onSelectionBoxChange, onSelectionBoxDone,
+ onSelectionScroll, onSelecting, onClickSelectionDone
+ ) => {
layoutSizeMayChangeEvent = layoutSizeMayChange;
return `<div class="groupSeries" ref="${ref}">
<div class="series" style="display:none;"></div>
- <div>${mainSeries(updateContainerWidth, onContainerScroll, onResize, layoutSizeMayChange)}</div>
- <div style="display:none">${subSerieses.map((subSeries) => subSeries(updateContainerWidth, onContainerScroll, onResize, layoutSizeMayChange)).join("")}</div>
+ <div>${mainSeries(updateContainerWidth, onContainerScroll, onResize,
+ layoutSizeMayChange, onSelectionBoxChange, onSelectionBoxDone,
+ onSelectionScroll, onSelecting, onClickSelectionDone
+ )}</div>
+ <div style="display:none">${subSerieses.map((subSeries) => subSeries(
+ updateContainerWidth, onContainerScroll, onResize,
+ layoutSizeMayChange, onSelectionBoxChange, onSelectionBoxDone,
+ onSelectionScroll, onSelecting, onClickSelectionDone
+ )).join("")}</div>
</div>`;
});
}
@@ -885,7 +1180,7 @@
option.exporter(updateData);
onContainerScroll.action(onScrollAction);
onResize.action(onResizeAction);
- return `<div class="x-axis">
+ return `<div class="x-axis" style="user-select: none; -webkit-user-select: none;">
<canvas ref="${canvasRef}">
</div>`;
}),
@@ -894,7 +1189,7 @@
};
}
-Timeline.CanvasContainer = (exporter, ...children) => {
+Timeline.CanvasContainer = (props, exporter, ...children) => {
let headerAxisPlaceHolderHeight = 0;
let topAxis = true;
const upackChildren = (children) => {
@@ -929,7 +1224,7 @@
<div class="header" style="padding-top:${headerAxisPlaceHolderHeight}px">
${ListComponent(composer, ...headers)}
</div>
- ${XScrollableCanvasProvider(composer, ...serieses)}
+ ${XScrollableCanvasProvider(props, composer, ...serieses)}
</div>`
);
}