blob: 2c7aebd4bf361023786b52e60cdd54effa6ec411 [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/modules/media_controls/elements/media_control_input_element.h"
#include "third_party/blink/public/platform/web_size.h"
#include "third_party/blink/renderer/core/dom/dom_token_list.h"
#include "third_party/blink/renderer/core/dom/events/event.h"
#include "third_party/blink/renderer/core/html/forms/html_label_element.h"
#include "third_party/blink/renderer/core/html/html_div_element.h"
#include "third_party/blink/renderer/core/html/html_span_element.h"
#include "third_party/blink/renderer/core/html/media/html_media_element.h"
#include "third_party/blink/renderer/modules/media_controls/elements/media_control_elements_helper.h"
#include "third_party/blink/renderer/modules/media_controls/media_controls_impl.h"
#include "third_party/blink/renderer/platform/histogram.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/text/platform_locale.h"
namespace {
// The default size of an overflow button in pixels.
constexpr int kDefaultButtonSize = 48;
const char kOverflowContainerWithSubtitleCSSClass[] = "with-subtitle";
const char kOverflowSubtitleCSSClass[] = "subtitle";
} // namespace
namespace blink {
// static
bool MediaControlInputElement::ShouldRecordDisplayStates(
const HTMLMediaElement& media_element) {
// Only record when the metadat are available so that the display state of the
// buttons are fairly stable. For example, before metadata are available, the
// size of the element might differ, it's unknown if the file has an audio
// track, etc.
if (media_element.getReadyState() >= HTMLMediaElement::kHaveMetadata)
return true;
// When metadata are not available, only record the display state if the
// element will require a user gesture in order to load.
if (media_element.EffectivePreloadType() ==
WebMediaPlayer::Preload::kPreloadNone) {
return true;
}
return false;
}
HTMLElement* MediaControlInputElement::CreateOverflowElement(
MediaControlInputElement* button) {
if (!button)
return nullptr;
// We don't want the button visible within the overflow menu.
button->SetInlineStyleProperty(CSSPropertyDisplay, CSSValueNone);
overflow_menu_text_ = HTMLSpanElement::Create(GetDocument());
overflow_menu_text_->setInnerText(button->GetOverflowMenuString(),
ASSERT_NO_EXCEPTION);
HTMLLabelElement* element = HTMLLabelElement::Create(GetDocument());
element->SetShadowPseudoId(
AtomicString("-internal-media-controls-overflow-menu-list-item"));
// Appending a button to a label element ensures that clicks on the label
// are passed down to the button, performing the action we'd expect.
element->ParserAppendChild(button);
// Allows to focus the list entry instead of the button.
element->setTabIndex(0);
button->setTabIndex(-1);
if (MediaControlsImpl::IsModern()) {
overflow_menu_container_ = HTMLDivElement::Create(GetDocument());
overflow_menu_container_->ParserAppendChild(overflow_menu_text_);
UpdateOverflowSubtitleElement(button->GetOverflowMenuSubtitleString());
element->ParserAppendChild(overflow_menu_container_);
} else {
element->ParserAppendChild(overflow_menu_text_);
}
// Initialize the internal states of the main element and the overflow one.
button->is_overflow_element_ = true;
overflow_element_ = button;
// Keeping the element hidden by default. This is setting the style in
// addition of calling ShouldShowButtonInOverflowMenu() to guarantee that the
// internal state matches the CSS state.
element->SetInlineStyleProperty(CSSPropertyDisplay, CSSValueNone);
SetOverflowElementIsWanted(false);
return element;
}
void MediaControlInputElement::UpdateOverflowSubtitleElement(String text) {
DCHECK(overflow_menu_container_);
if (!text) {
// If setting the text to null, we want to remove the element.
RemoveOverflowSubtitleElement();
return;
}
if (overflow_menu_subtitle_) {
// If element exists, just update the text.
overflow_menu_subtitle_->setInnerText(text, ASSERT_NO_EXCEPTION);
} else {
// Otherwise, create a new element.
overflow_menu_subtitle_ = HTMLSpanElement::Create(GetDocument());
overflow_menu_subtitle_->setInnerText(text, ASSERT_NO_EXCEPTION);
overflow_menu_subtitle_->setAttribute("class", kOverflowSubtitleCSSClass);
overflow_menu_container_->ParserAppendChild(overflow_menu_subtitle_);
overflow_menu_container_->setAttribute(
"class", kOverflowContainerWithSubtitleCSSClass);
}
}
void MediaControlInputElement::RemoveOverflowSubtitleElement() {
if (!overflow_menu_subtitle_)
return;
overflow_menu_container_->RemoveChild(overflow_menu_subtitle_);
overflow_menu_container_->removeAttribute("class");
overflow_menu_subtitle_ = nullptr;
}
bool MediaControlInputElement::OverflowElementIsWanted() {
return overflow_element_ && overflow_element_->IsWanted();
}
void MediaControlInputElement::SetOverflowElementIsWanted(bool wanted) {
if (!overflow_element_)
return;
overflow_element_->SetIsWanted(wanted);
}
void MediaControlInputElement::MaybeRecordDisplayed() {
// Display is defined as wanted and fitting. Overflow elements will only be
// displayed if their inline counterpart isn't displayed.
if (!IsWanted() || !DoesFit()) {
if (IsWanted() && overflow_element_)
overflow_element_->MaybeRecordDisplayed();
return;
}
// Keep this check after the block above because `display_recorded_` might be
// true for the inline element but not for the overflow one.
if (display_recorded_)
return;
RecordCTREvent(CTREvent::kDisplayed);
display_recorded_ = true;
}
void MediaControlInputElement::UpdateOverflowString() {
if (!overflow_menu_text_)
return;
DCHECK(overflow_element_);
overflow_menu_text_->setInnerText(GetOverflowMenuString(),
ASSERT_NO_EXCEPTION);
if (MediaControlsImpl::IsModern())
UpdateOverflowSubtitleElement(GetOverflowMenuSubtitleString());
}
MediaControlInputElement::MediaControlInputElement(
MediaControlsImpl& media_controls,
MediaControlElementType display_type)
: HTMLInputElement(media_controls.GetDocument(), CreateElementFlags()),
MediaControlElementBase(media_controls, display_type, this) {
CreateUserAgentShadowRoot();
CreateShadowSubtree();
}
WebLocalizedString::Name MediaControlInputElement::GetOverflowStringName()
const {
NOTREACHED();
return WebLocalizedString::kAXAMPMFieldText;
}
void MediaControlInputElement::UpdateShownState() {
if (is_overflow_element_) {
Element* parent = parentElement();
DCHECK(parent);
DCHECK(IsHTMLLabelElement(parent));
if (IsWanted() && DoesFit())
parent->RemoveInlineStyleProperty(CSSPropertyDisplay);
else
parent->SetInlineStyleProperty(CSSPropertyDisplay, CSSValueNone);
// Don't update the shown state of the element if we want to hide
// icons on the overflow menu.
if (!RuntimeEnabledFeatures::OverflowIconsForMediaControlsEnabled())
return;
}
MediaControlElementBase::UpdateShownState();
}
void MediaControlInputElement::DefaultEventHandler(Event& event) {
if (event.type() == event_type_names::kClick)
MaybeRecordInteracted();
HTMLInputElement::DefaultEventHandler(event);
}
void MediaControlInputElement::MaybeRecordInteracted() {
if (interaction_recorded_)
return;
if (!display_recorded_) {
// The only valid reason to not have the display recorded at this point is
// if it wasn't allowed. Regardless, the display will now be recorded.
DCHECK(!ShouldRecordDisplayStates(MediaElement()));
RecordCTREvent(CTREvent::kDisplayed);
}
RecordCTREvent(CTREvent::kInteracted);
interaction_recorded_ = true;
}
bool MediaControlInputElement::IsOverflowElement() const {
return is_overflow_element_;
}
bool MediaControlInputElement::IsMouseFocusable() const {
return false;
}
bool MediaControlInputElement::IsMediaControlElement() const {
return true;
}
String MediaControlInputElement::GetOverflowMenuString() const {
return MediaElement().GetLocale().QueryString(GetOverflowStringName());
}
String MediaControlInputElement::GetOverflowMenuSubtitleString() const {
return String();
}
void MediaControlInputElement::RecordCTREvent(CTREvent event) {
String histogram_name("Media.Controls.CTR.");
histogram_name.append(GetNameForHistograms());
EnumerationHistogram ctr_histogram(histogram_name.Ascii().data(),
static_cast<int>(CTREvent::kCount));
ctr_histogram.Count(static_cast<int>(event));
}
void MediaControlInputElement::SetClass(const AtomicString& class_name,
bool should_have_class) {
if (should_have_class)
classList().Add(class_name);
else
classList().Remove(class_name);
}
void MediaControlInputElement::UpdateDisplayType() {
if (overflow_element_)
overflow_element_->UpdateDisplayType();
}
WebSize MediaControlInputElement::GetSizeOrDefault() const {
if (HasOverflowButton()) {
// If this has an overflow button then it is a button control and therefore
// has a default size of kDefaultButtonSize.
return MediaControlElementsHelper::GetSizeOrDefault(
*this, WebSize(kDefaultButtonSize, kDefaultButtonSize));
}
return MediaControlElementsHelper::GetSizeOrDefault(*this, WebSize(0, 0));
}
bool MediaControlInputElement::IsDisabled() const {
return hasAttribute(html_names::kDisabledAttr);
}
void MediaControlInputElement::Trace(blink::Visitor* visitor) {
HTMLInputElement::Trace(visitor);
MediaControlElementBase::Trace(visitor);
visitor->Trace(overflow_element_);
visitor->Trace(overflow_menu_container_);
visitor->Trace(overflow_menu_text_);
visitor->Trace(overflow_menu_subtitle_);
}
} // namespace blink