Commit 7f08061e authored by Alessandro Chitolina's avatar Alessandro Chitolina Committed by XhmikosR
Browse files

rewritten tab without jquery

parent 90261b48
main cleanup-floating-forms cssvar-function dependabot/npm_and_yarn/stylelint-and-stylelint-config-twbs-bootstrap-15.3.0 extend-snippets feat/data-target floating-always-visible floating-labels-icons fod-main-banner form-controls-with-icons github/fork/719media/patch-13 github/fork/719media/patch-14 github/fork/719media/patch-9 github/fork/ChellyAhmed/fix-typo-reboot.md github/fork/ChellyAhmed/offcanvas-scroll-back github/fork/CtrlAltLilith/main github/fork/Elysiome/offcanvas-optional-window-resizing github/fork/JanSargsyan/main github/fork/LunicLynx/support-different-line-height-for-buttons github/fork/Psixodelik/main github/fork/Ronid1/ronid1/offcanvas_static_backdrop github/fork/RyanBerliner/tooltip-accessibility github/fork/SantiagoPVazquez/Feature-default-border-bottom-to-dropdown-item github/fork/Sir-Genius/utils github/fork/Sumit-Singh-8/main github/fork/Viktor-VERA2020/offcanvas-slide github/fork/Zivangu9/input-group-for-form-control-plaintext github/fork/alpadev/alpadev/call-dispose-on-component-reinstantiation github/fork/astagi/fix/tree-shake-modules github/fork/compnerd/dark-accordion-icon github/fork/derSascha/dropdown-dont-close-on-input-click github/fork/dev-ph1l/main github/fork/donquixote/issue-33861-utl-mixin github/fork/florianlacreuse/mixin-make-row-gutter-y github/fork/gregorw/main github/fork/iteggmbh/transitionend-dispose-race github/fork/jdelStrother/patch-1 github/fork/jonnysp/form-floating github/fork/jonnysp/independent-offcanvas github/fork/jonnysp/theme-dark-on-card-and-modal-fix github/fork/josefdlange/floating-label-placeholder-opacity github/fork/julien-deramond/enhance-change-version.js github/fork/julien-deramond/main-jd-fix-offset-content github/fork/julien-deramond/main-jd-issue-with-utitlies github/fork/julien-deramond/main-xmr-pa11y-ci-jd-add-hideElements github/fork/kyletsang/fix-tooltip-padding github/fork/lacutah/CheckboxCenteringDocumentation github/fork/lekoala/patch-3 github/fork/louismaximepiton/main-kld-lmp-collapse-proposal github/fork/louismaximepiton/main-lmp-card-inner-border-radius-fix github/fork/louismaximepiton/main-lmp-carousel-multiple-images github/fork/louismaximepiton/main-lmp-css-var-init github/fork/louismaximepiton/main-lmp-disabled-floating-label-fix github/fork/louismaximepiton/main-lmp-input-range-fix github/fork/louismaximepiton/main-lmp-shift-color github/fork/louismaximepiton/main-lmp-table-active-tr-fix github/fork/maciek-szn/switch github/fork/michael-roth/feature/19964-multiple-tab-targets github/fork/mistic100/dom-utils github/fork/nkdas91/accordion github/fork/nstungcom/fix-missing-modal-open-class github/fork/oraliahdz/animation-utilities github/fork/pine3ree/patch-7 github/fork/pouwerkerk/unindent-scss-docs-shortcode github/fork/smares/smares-no-scolling-on-modal-close github/fork/tgm-git/patch-1 gs-forms gs-toasts-with-animated-progress-bar gs/add-history-helper gs/change-version-dir-on-docs gs/data-must-set-onlu-one-instance gs/docs/fix-drop-down-error gs/event-handler-2 gs/make-docs-js-build gs/make-simple-attribute-toggler gs/popover-fix-doc gs/provide-steConfig-method gs/scrollspy-smoothscroll-option-use-browser-history gs/streamline-jqueryInterface gs/support-drop-down-in-navbar gs/test-js-generic-trigger gs/try-web-components gs/tweak-collapse-js-selector gs/use-event-handler-in-cocmponent gs/use-rollup-replace-for-version jo-docs-thanks-page jo-ssr-friendly logical-props-spacing-utils main-fod-disabled-form-check-label main-fod-nested-accordion main-fod-simpler-table-structure main-fod-table-separator main-fod-utilities-contrast main-jd-abbr-title main-jd-add-chips main-jd-add-doc-for-sass-custom-colors main-jd-add-enable-host-to-handle-web-components main-jd-browserstack-fine-tune main-jd-browserstack-updates main-jd-docs-consistent-usage-of-css-sections-step-2 main-jd-fix-docs-headers-in-white main-jd-fix-highlight-docs-border-radius main-jd-fix-placeholder-color-background-params-for-img-markup main-jd-glossary-experiment main-jd-postcss-drop-empty-css-vars main-jd-proto-doc-astro main-jd-skip-navigation-component main-jd-stackblitz-for-examples main-jd-upgrade-browserlistrc main-jd-use-host main-lmp-dark-theme-customization main-lmp-handle-scroll-target main-lmp-tab-fix main-mc-opensearch main-xmr-bundlewatch-action main-xmr-eslint-plugin-compat main-xmr-hugo-docs-vendor main-xmr-hugo-rm-ver main-xmr-linkinator-prod main-xmr-min-mangle main-xmr-pa11y-ci more-darkmode-examples nested-dropdowns patrickhlauke-issue37428 patrickhlauke-use-of-color-tweaks pr/34102 pr/37590 previous-next-docs-links sticky-thead utilities-functions-mixin v530-dev v6-postcss-custom-media v6-spinner-dots v6/gs/use-floating-ui-in-place-of-popper xmr/dev xmr/docs-png xmr/docs-svgs xmr/hugo-reorg-files xmr/js-2 xmr/markdownlint xmr/prepare-530-alpha2 xmr/xo v5.3.0-alpha1 v5.2.3 v5.2.2 v5.2.1 v5.2.0 v5.2.0-beta1 v5.1.3 v5.1.2 v5.1.1 v5.1.0 v5.0.2 v5.0.1 v5.0.0 v5.0.0-beta3 v5.0.0-beta2 v5.0.0-beta1 v5.0.0-alpha3 v5.0.0-alpha2 v5.0.0-alpha1
5 merge requests!31948Examples/Floating-labels: fix bad behavior with autofill,!30064test,!29779Responsive sizing,!28882fix custom-select-indicator in IE10,!28721Hot test
Showing with 137 additions and 81 deletions
+137 -81
import Util from '../util'
/** /**
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------
* Bootstrap (v4.0.0-beta): dom/selectorEngine.js * Bootstrap (v4.0.0-beta): dom/selectorEngine.js
...@@ -27,17 +29,13 @@ const SelectorEngine = (() => { ...@@ -27,17 +29,13 @@ const SelectorEngine = (() => {
if (!Element.prototype.closest) { if (!Element.prototype.closest) {
fnClosest = (element, selector) => { fnClosest = (element, selector) => {
let ancestor = element let ancestor = element
if (!document.documentElement.contains(element)) {
return null
}
do { do {
if (fnMatches.call(ancestor, selector)) { if (fnMatches.call(ancestor, selector)) {
return ancestor return ancestor
} }
ancestor = ancestor.parentElement ancestor = ancestor.parentElement
} while (ancestor !== null) } while (ancestor !== null && ancestor.nodeType === Node.ELEMENT_NODE)
return null return null
} }
...@@ -48,12 +46,67 @@ const SelectorEngine = (() => { ...@@ -48,12 +46,67 @@ const SelectorEngine = (() => {
} }
} }
const scopeSelectorRegex = /:scope\b/
const supportScopeQuery = (() => {
const element = document.createElement('div')
try {
element.querySelectorAll(':scope *')
} catch (e) {
return false
}
return true
})()
let findFn = null
let findOneFn = null
if (supportScopeQuery) {
findFn = Element.prototype.querySelectorAll
findOneFn = Element.prototype.querySelector
} else {
findFn = function (selector) {
if (!scopeSelectorRegex.test(selector)) {
return this.querySelectorAll(selector)
}
const hasId = Boolean(this.id)
if (!hasId) {
this.id = Util.getUID('scope')
}
let nodeList = null
try {
selector = selector.replace(scopeSelectorRegex, `#${this.id}`)
nodeList = this.querySelectorAll(selector)
} finally {
if (!hasId) {
this.removeAttribute('id')
}
}
return nodeList
}
findOneFn = function (selector) {
if (!scopeSelectorRegex.test(selector)) {
return this.querySelector(selector)
}
const matches = findFn.call(this, selector)
if (typeof matches[0] !== 'undefined') {
return matches[0]
}
return null
}
}
return { return {
matches(element, selector) { matches(element, selector) {
return fnMatches.call(element, selector) return fnMatches.call(element, selector)
}, },
find(selector, element = document) { find(selector, element = document.documentElement) {
if (typeof selector !== 'string') { if (typeof selector !== 'string') {
return null return null
} }
...@@ -62,21 +115,24 @@ const SelectorEngine = (() => { ...@@ -62,21 +115,24 @@ const SelectorEngine = (() => {
return SelectorEngine.findOne(selector, element) return SelectorEngine.findOne(selector, element)
} }
return element.querySelectorAll(selector) return findFn.call(element, selector)
}, },
findOne(selector, element = document) { findOne(selector, element = document.documentElement) {
if (typeof selector !== 'string') { if (typeof selector !== 'string') {
return null return null
} }
let selectorType = 'querySelector' return findOneFn.call(element, selector)
if (selector.indexOf('#') === 0) { },
selectorType = 'getElementById'
selector = selector.substr(1, selector.length) children(element, selector) {
if (typeof selector !== 'string') {
return null
} }
return element[selectorType](selector) const children = Util.makeArray(element.children)
return children.filter((child) => this.matches(child, selector))
}, },
closest(element, selector) { closest(element, selector) {
......
...@@ -5,7 +5,9 @@ ...@@ -5,7 +5,9 @@
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------
*/ */
import $ from 'jquery' import Data from './dom/data'
import EventHandler from './dom/eventHandler'
import SelectorEngine from './dom/selectorEngine'
import Util from './util' import Util from './util'
/** /**
...@@ -19,7 +21,6 @@ const VERSION = '4.3.1' ...@@ -19,7 +21,6 @@ const VERSION = '4.3.1'
const DATA_KEY = 'bs.tab' const DATA_KEY = 'bs.tab'
const EVENT_KEY = `.${DATA_KEY}` const EVENT_KEY = `.${DATA_KEY}`
const DATA_API_KEY = '.data-api' const DATA_API_KEY = '.data-api'
const JQUERY_NO_CONFLICT = $.fn[NAME]
const Event = { const Event = {
HIDE : `hide${EVENT_KEY}`, HIDE : `hide${EVENT_KEY}`,
...@@ -41,10 +42,10 @@ const Selector = { ...@@ -41,10 +42,10 @@ const Selector = {
DROPDOWN : '.dropdown', DROPDOWN : '.dropdown',
NAV_LIST_GROUP : '.nav, .list-group', NAV_LIST_GROUP : '.nav, .list-group',
ACTIVE : '.active', ACTIVE : '.active',
ACTIVE_UL : '> li > .active', ACTIVE_UL : ':scope > li > .active',
DATA_TOGGLE : '[data-toggle="tab"], [data-toggle="pill"], [data-toggle="list"]', DATA_TOGGLE : '[data-toggle="tab"], [data-toggle="pill"], [data-toggle="list"]',
DROPDOWN_TOGGLE : '.dropdown-toggle', DROPDOWN_TOGGLE : '.dropdown-toggle',
DROPDOWN_ACTIVE_CHILD : '> .dropdown-menu .active' DROPDOWN_ACTIVE_CHILD : ':scope > .dropdown-menu .active'
} }
/** /**
...@@ -56,6 +57,8 @@ const Selector = { ...@@ -56,6 +57,8 @@ const Selector = {
class Tab { class Tab {
constructor(element) { constructor(element) {
this._element = element this._element = element
Data.setData(this._element, DATA_KEY, this)
} }
// Getters // Getters
...@@ -68,39 +71,37 @@ class Tab { ...@@ -68,39 +71,37 @@ class Tab {
show() { show() {
if (this._element.parentNode && if (this._element.parentNode &&
this._element.parentNode.nodeType === Node.ELEMENT_NODE && this._element.parentNode.nodeType === Node.ELEMENT_NODE &&
$(this._element).hasClass(ClassName.ACTIVE) || this._element.classList.contains(ClassName.ACTIVE) ||
$(this._element).hasClass(ClassName.DISABLED)) { this._element.classList.contains(ClassName.DISABLED)) {
return return
} }
let target let target
let previous let previous
const listElement = $(this._element).closest(Selector.NAV_LIST_GROUP)[0] const listElement = SelectorEngine.closest(this._element, Selector.NAV_LIST_GROUP)
const selector = Util.getSelectorFromElement(this._element) const selector = Util.getSelectorFromElement(this._element)
if (listElement) { if (listElement) {
const itemSelector = listElement.nodeName === 'UL' || listElement.nodeName === 'OL' ? Selector.ACTIVE_UL : Selector.ACTIVE const itemSelector = listElement.nodeName === 'UL' || listElement.nodeName === 'OL' ? Selector.ACTIVE_UL : Selector.ACTIVE
previous = $.makeArray($(listElement).find(itemSelector)) previous = Util.makeArray(SelectorEngine.find(itemSelector, listElement))
previous = previous[previous.length - 1] previous = previous[previous.length - 1]
} }
const hideEvent = $.Event(Event.HIDE, { let hideEvent = null
relatedTarget: this._element
})
const showEvent = $.Event(Event.SHOW, {
relatedTarget: previous
})
if (previous) { if (previous) {
$(previous).trigger(hideEvent) hideEvent = EventHandler.trigger(previous, Event.HIDE, {
relatedTarget: this._element
})
} }
$(this._element).trigger(showEvent) const showEvent = EventHandler.trigger(this._element, Event.SHOW, {
relatedTarget: previous
})
if (showEvent.isDefaultPrevented() || if (showEvent.defaultPrevented ||
hideEvent.isDefaultPrevented()) { hideEvent !== null && hideEvent.defaultPrevented) {
return return
} }
...@@ -114,16 +115,12 @@ class Tab { ...@@ -114,16 +115,12 @@ class Tab {
) )
const complete = () => { const complete = () => {
const hiddenEvent = $.Event(Event.HIDDEN, { EventHandler.trigger(previous, Event.HIDDEN, {
relatedTarget: this._element relatedTarget: this._element
}) })
EventHandler.trigger(this._element, Event.SHOWN, {
const shownEvent = $.Event(Event.SHOWN, {
relatedTarget: previous relatedTarget: previous
}) })
$(previous).trigger(hiddenEvent)
$(this._element).trigger(shownEvent)
} }
if (target) { if (target) {
...@@ -134,7 +131,7 @@ class Tab { ...@@ -134,7 +131,7 @@ class Tab {
} }
dispose() { dispose() {
$.removeData(this._element, DATA_KEY) Data.removeData(this._element, DATA_KEY)
this._element = null this._element = null
} }
...@@ -142,11 +139,13 @@ class Tab { ...@@ -142,11 +139,13 @@ class Tab {
_activate(element, container, callback) { _activate(element, container, callback) {
const activeElements = container && (container.nodeName === 'UL' || container.nodeName === 'OL') const activeElements = container && (container.nodeName === 'UL' || container.nodeName === 'OL')
? $(container).find(Selector.ACTIVE_UL) ? SelectorEngine.find(Selector.ACTIVE_UL, container)
: $(container).children(Selector.ACTIVE) : SelectorEngine.children(container, Selector.ACTIVE)
const active = activeElements[0]
const isTransitioning = callback &&
(active && active.classList.contains(ClassName.FADE))
const active = activeElements[0]
const isTransitioning = callback && (active && $(active).hasClass(ClassName.FADE))
const complete = () => this._transitionComplete( const complete = () => this._transitionComplete(
element, element,
active, active,
...@@ -155,10 +154,9 @@ class Tab { ...@@ -155,10 +154,9 @@ class Tab {
if (active && isTransitioning) { if (active && isTransitioning) {
const transitionDuration = Util.getTransitionDurationFromElement(active) const transitionDuration = Util.getTransitionDurationFromElement(active)
active.classList.remove(ClassName.SHOW)
$(active) EventHandler.one(active, Util.TRANSITION_END, complete)
.removeClass(ClassName.SHOW)
.one(Util.TRANSITION_END, complete)
Util.emulateTransitionEnd(active, transitionDuration) Util.emulateTransitionEnd(active, transitionDuration)
} else { } else {
complete() complete()
...@@ -167,14 +165,12 @@ class Tab { ...@@ -167,14 +165,12 @@ class Tab {
_transitionComplete(element, active, callback) { _transitionComplete(element, active, callback) {
if (active) { if (active) {
$(active).removeClass(ClassName.ACTIVE) active.classList.remove(ClassName.ACTIVE)
const dropdownChild = $(active.parentNode).find( const dropdownChild = SelectorEngine.findOne(Selector.DROPDOWN_ACTIVE_CHILD, active.parentNode)
Selector.DROPDOWN_ACTIVE_CHILD
)[0]
if (dropdownChild) { if (dropdownChild) {
$(dropdownChild).removeClass(ClassName.ACTIVE) dropdownChild.classList.remove(ClassName.ACTIVE)
} }
if (active.getAttribute('role') === 'tab') { if (active.getAttribute('role') === 'tab') {
...@@ -182,7 +178,7 @@ class Tab { ...@@ -182,7 +178,7 @@ class Tab {
} }
} }
$(element).addClass(ClassName.ACTIVE) element.classList.add(ClassName.ACTIVE)
if (element.getAttribute('role') === 'tab') { if (element.getAttribute('role') === 'tab') {
element.setAttribute('aria-selected', true) element.setAttribute('aria-selected', true)
} }
...@@ -193,13 +189,12 @@ class Tab { ...@@ -193,13 +189,12 @@ class Tab {
element.classList.add(ClassName.SHOW) element.classList.add(ClassName.SHOW)
} }
if (element.parentNode && $(element.parentNode).hasClass(ClassName.DROPDOWN_MENU)) { if (element.parentNode && element.parentNode.classList.contains(ClassName.DROPDOWN_MENU)) {
const dropdownElement = $(element).closest(Selector.DROPDOWN)[0] const dropdownElement = SelectorEngine.closest(element, Selector.DROPDOWN)
if (dropdownElement) { if (dropdownElement) {
const dropdownToggleList = [].slice.call(dropdownElement.querySelectorAll(Selector.DROPDOWN_TOGGLE)) Util.makeArray(dropdownElement.querySelectorAll(Selector.DROPDOWN_TOGGLE))
.forEach((dropdown) => dropdown.classList.add(ClassName.ACTIVE))
$(dropdownToggleList).addClass(ClassName.ACTIVE)
} }
element.setAttribute('aria-expanded', true) element.setAttribute('aria-expanded', true)
...@@ -214,13 +209,7 @@ class Tab { ...@@ -214,13 +209,7 @@ class Tab {
static _jQueryInterface(config) { static _jQueryInterface(config) {
return this.each(function () { return this.each(function () {
const $this = $(this) const data = Data.getData(this, DATA_KEY) || new Tab(this)
let data = $this.data(DATA_KEY)
if (!data) {
data = new Tab(this)
$this.data(DATA_KEY, data)
}
if (typeof config === 'string') { if (typeof config === 'string') {
if (typeof data[config] === 'undefined') { if (typeof data[config] === 'undefined') {
...@@ -238,11 +227,12 @@ class Tab { ...@@ -238,11 +227,12 @@ class Tab {
* ------------------------------------------------------------------------ * ------------------------------------------------------------------------
*/ */
$(document) EventHandler.on(document, Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) {
.on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) { event.preventDefault()
event.preventDefault()
Tab._jQueryInterface.call($(this), 'show') const data = Data.getData(this, DATA_KEY) || new Tab(this)
}) data.show()
})
/** /**
* ------------------------------------------------------------------------ * ------------------------------------------------------------------------
...@@ -250,11 +240,15 @@ $(document) ...@@ -250,11 +240,15 @@ $(document)
* ------------------------------------------------------------------------ * ------------------------------------------------------------------------
*/ */
$.fn[NAME] = Tab._jQueryInterface const $ = Util.jQuery
$.fn[NAME].Constructor = Tab if (typeof $ !== 'undefined') {
$.fn[NAME].noConflict = () => { const JQUERY_NO_CONFLICT = $.fn[NAME]
$.fn[NAME] = JQUERY_NO_CONFLICT $.fn[NAME] = Tab._jQueryInterface
return Tab._jQueryInterface $.fn[NAME].Constructor = Tab
$.fn[NAME].noConflict = () => {
$.fn[NAME] = JQUERY_NO_CONFLICT
return Tab._jQueryInterface
}
} }
export default Tab export default Tab
...@@ -320,7 +320,7 @@ $(function () { ...@@ -320,7 +320,7 @@ $(function () {
'</ul>' '</ul>'
var $tabs = $(tabsHTML).appendTo('#qunit-fixture') var $tabs = $(tabsHTML).appendTo('#qunit-fixture')
$tabs.find('li:last-child a').trigger('click') EventHandler.trigger($tabs.find('li:last-child a')[0], 'click')
assert.notOk($tabs.find('li:first-child a').hasClass('active')) assert.notOk($tabs.find('li:first-child a').hasClass('active'))
assert.ok($tabs.find('li:last-child a').hasClass('active')) assert.ok($tabs.find('li:last-child a').hasClass('active'))
}) })
...@@ -339,7 +339,7 @@ $(function () { ...@@ -339,7 +339,7 @@ $(function () {
'</ul>' '</ul>'
var $tabs = $(tabsHTML).appendTo('#qunit-fixture') var $tabs = $(tabsHTML).appendTo('#qunit-fixture')
$tabs.find('li:first-child a').trigger('click') EventHandler.trigger($tabs.find('li:first-child a')[0], 'click')
assert.ok($tabs.find('li:first-child a').hasClass('active')) assert.ok($tabs.find('li:first-child a').hasClass('active'))
assert.notOk($tabs.find('li:last-child a').hasClass('active')) assert.notOk($tabs.find('li:last-child a').hasClass('active'))
assert.notOk($tabs.find('li:last-child .dropdown-menu a:first-child').hasClass('active')) assert.notOk($tabs.find('li:last-child .dropdown-menu a:first-child').hasClass('active'))
...@@ -378,9 +378,10 @@ $(function () { ...@@ -378,9 +378,10 @@ $(function () {
$('#tab1').on('shown.bs.tab', function () { $('#tab1').on('shown.bs.tab', function () {
assert.ok($('#x-tab1').hasClass('active')) assert.ok($('#x-tab1').hasClass('active'))
$('#tabNested2').trigger($.Event('click')) EventHandler.trigger($('#tabNested2')[0], 'click')
}) })
.trigger($.Event('click'))
EventHandler.trigger($('#tab1')[0], 'click')
}) })
QUnit.test('should not remove fade class if no active pane is present', function (assert) { QUnit.test('should not remove fade class if no active pane is present', function (assert) {
...@@ -410,9 +411,11 @@ $(function () { ...@@ -410,9 +411,11 @@ $(function () {
done() done()
}) })
.trigger($.Event('click'))
EventHandler.trigger($('#tab-home')[0], 'click')
}) })
.trigger($.Event('click'))
EventHandler.trigger($('#tab-profile')[0], 'click')
}) })
QUnit.test('should handle removed tabs', function (assert) { QUnit.test('should handle removed tabs', function (assert) {
......
...@@ -227,7 +227,10 @@ ...@@ -227,7 +227,10 @@
<script src="../../../node_modules/jquery/dist/jquery.slim.min.js"></script> <script src="../../../node_modules/jquery/dist/jquery.slim.min.js"></script>
<script src="../../../node_modules/popper.js/dist/umd/popper.min.js"></script> <script src="../../../node_modules/popper.js/dist/umd/popper.min.js"></script>
<script src="../../dist/dom/data.js"></script>
<script src="../../dist/dom/eventHandler.js"></script> <script src="../../dist/dom/eventHandler.js"></script>
<script src="../../dist/dom/manipulator.js"></script>
<script src="../../dist/dom/selectorEngine.js"></script>
<script src="../../dist/util.js"></script> <script src="../../dist/util.js"></script>
<script src="../../dist/tab.js"></script> <script src="../../dist/tab.js"></script>
<script src="../../dist/dropdown.js"></script> <script src="../../dist/dropdown.js"></script>
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment