rx-player
Version:
Canal+ HTML5 Video Player
553 lines (500 loc) • 17.2 kB
text/typescript
/**
* Copyright 2015 CANAL+ Group
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import addClassName from "../../../../compat/add_class_name";
import isNonEmptyString from "../../../../utils/is_non_empty_string";
import objectAssign from "../../../../utils/object_assign";
import type { IStyleList, IStyleObject } from "../get_styling";
import { getStylingAttributes } from "../get_styling";
import { getParentDivElements, isLineBreakElement, isSpanElement } from "../xml_utils";
import applyExtent from "./apply_extent";
import applyFontSize from "./apply_font_size";
import applyLineHeight from "./apply_line_height";
import applyOrigin from "./apply_origin";
import applyPadding from "./apply_padding";
import generateCSSTextOutline from "./generate_css_test_outline";
import ttmlColorToCSSColor from "./ttml_color_to_css_color";
// Styling which can be applied to <span> from any level upper.
// Added here as an optimization
const SPAN_LEVEL_ATTRIBUTES = [
"color",
"direction",
"display",
"fontFamily",
"fontSize",
"fontStyle",
"fontWeight",
"textDecoration",
"textOutline",
"unicodeBidi",
"visibility",
"wrapOption",
];
// TODO
// tts:showBackground (applies to region)
// tts:zIndex (applies to region)
/**
* Apply style set for a singular text span of the current cue.
* @param {HTMLElement} element - The text span
* @param {Object} style - The style to apply
*/
function applyTextStyle(
element: HTMLElement,
style: Partial<Record<string, string>>,
shouldTrimWhiteSpace: boolean,
) {
// applies to span
const color = style.color;
if (isNonEmptyString(color)) {
element.style.color = ttmlColorToCSSColor(color);
}
// applies to body, div, p, region, span
const backgroundColor = style.backgroundColor;
if (isNonEmptyString(backgroundColor)) {
element.style.backgroundColor = ttmlColorToCSSColor(backgroundColor);
}
// applies to span
const textOutline = style.textOutline;
if (isNonEmptyString(textOutline)) {
const outlineData = textOutline.trim().replace(/\s+/g, " ").split(" ");
const len = outlineData.length;
if (len === 3) {
const outlineColor = ttmlColorToCSSColor(outlineData[0]);
const thickness = outlineData[1];
element.style.textShadow = generateCSSTextOutline(outlineColor, thickness);
} else if (isNonEmptyString(color) && len === 1) {
const thickness = outlineData[0];
element.style.textShadow = generateCSSTextOutline(color, thickness);
} else if (len === 2) {
const isFirstArgAColor = /^[#A-Z]/i.test(outlineData[0]);
const isFirstArgANumber = /^[0-9]/.test(outlineData[0]);
// XOR-ing to be sure we get what we have
if (isFirstArgAColor !== isFirstArgANumber) {
if (isFirstArgAColor) {
const outlineColor = ttmlColorToCSSColor(outlineData[0]);
const thickness = outlineData[1];
element.style.textShadow = generateCSSTextOutline(outlineColor, thickness);
} else if (isNonEmptyString(color)) {
const thickness = outlineData[0];
element.style.textShadow = generateCSSTextOutline(color, thickness);
}
}
}
}
// applies to span
const textDecoration = style.textDecoration;
if (isNonEmptyString(textDecoration)) {
switch (textDecoration) {
case "noUnderline":
case "noLineThrough":
case "noOverline":
element.style.textDecoration = "none";
break;
case "lineThrough":
element.style.textDecoration = "line-through";
break;
default:
element.style.textDecoration = textDecoration;
break;
}
}
// applies to span
const fontFamily = style.fontFamily;
if (isNonEmptyString(fontFamily)) {
switch (fontFamily) {
case "proportionalSansSerif":
element.style.fontFamily = "Arial, Helvetica, Liberation Sans, sans-serif";
break;
// TODO monospace or sans-serif or font with both?
case "monospaceSansSerif":
case "sansSerif":
element.style.fontFamily = "sans-serif";
break;
case "monospaceSerif":
case "default":
element.style.fontFamily = "Courier New, Liberation Mono, monospace";
break;
// TODO font with both?
case "proportionalSerif":
element.style.fontFamily = "serif";
break;
default:
element.style.fontFamily = fontFamily;
}
}
// applies to span
const fontStyle = style.fontStyle;
if (isNonEmptyString(fontStyle)) {
element.style.fontStyle = fontStyle;
}
// applies to span
const fontWeight = style.fontWeight;
if (isNonEmptyString(fontWeight)) {
element.style.fontWeight = fontWeight;
}
// applies to span
const fontSize = style.fontSize;
if (isNonEmptyString(fontSize)) {
applyFontSize(element, fontSize);
} else {
addClassName(element, "proportional-style");
element.setAttribute("data-proportional-font-size", "1");
}
// applies to p, span
const direction = style.direction;
if (isNonEmptyString(direction)) {
element.style.direction = direction;
}
// applies to p, span
const unicodeBidi = style.unicodeBidi;
if (isNonEmptyString(unicodeBidi)) {
switch (unicodeBidi) {
case "bidiOverride":
element.style.unicodeBidi = "bidi-override";
break;
case "embed":
element.style.unicodeBidi = "embed";
break;
default:
element.style.unicodeBidi = "normal";
}
}
// applies to body, div, p, region, span
const visibility = style.visibility;
if (isNonEmptyString(visibility)) {
element.style.visibility = visibility;
}
// applies to body, div, p, region, span
const display = style.display;
if (display === "none") {
element.style.display = "none";
}
// applies to body, div, p, region, span
const wrapOption = style.wrapOption;
if (wrapOption === "noWrap") {
if (shouldTrimWhiteSpace) {
element.style.whiteSpace = "nowrap";
} else {
element.style.whiteSpace = "pre";
}
} else if (shouldTrimWhiteSpace) {
element.style.whiteSpace = "normal";
} else {
element.style.whiteSpace = "pre-wrap";
}
}
/**
* Apply style for the general text track div.
* @param {HTMLElement} element - The <div> the style will be applied on.
* @param {Object} style - The general style object of the paragraph.
*/
function applyGeneralStyle(element: HTMLElement, style: Partial<Record<string, string>>) {
// Set default text color. It can be overrided by text element color.
element.style.color = "white";
element.style.position = "absolute";
// applies to tt, region
const extent = style.extent;
if (isNonEmptyString(extent)) {
applyExtent(element, extent);
}
// applies to region
const writingMode = style.writingMode;
if (isNonEmptyString(writingMode)) {
// TODO
}
// applies to region
const overflow = style.overflow;
element.style.overflow = isNonEmptyString(overflow) ? overflow : "hidden";
// applies to region
const padding = style.padding;
if (isNonEmptyString(padding)) {
applyPadding(element, padding);
}
// applies to region
const origin = style.origin;
if (isNonEmptyString(origin)) {
applyOrigin(element, origin);
}
// applies to region
const displayAlign = style.displayAlign;
if (isNonEmptyString(displayAlign)) {
element.style.display = "flex";
element.style.flexDirection = "column";
switch (displayAlign) {
case "before":
element.style.justifyContent = "flex-start";
break;
case "center":
element.style.justifyContent = "center";
break;
case "after":
element.style.justifyContent = "flex-end";
break;
}
}
// applies to region
const opacity = style.opacity;
if (isNonEmptyString(opacity)) {
element.style.opacity = opacity;
}
// applies to body, div, p, region, span
const visibility = style.visibility;
if (isNonEmptyString(visibility)) {
element.style.visibility = visibility;
}
// applies to body, div, p, region, span
const display = style.display;
if (display === "none") {
element.style.display = "none";
}
}
/**
* Apply style set for a <p> element
* @param {HTMLElement} element - The <p> element
* @param {Object} style - The general style object of the paragraph.
*/
function applyPStyle(element: HTMLElement, style: Partial<Record<string, string>>) {
element.style.margin = "0px";
// Set on it the default font-size, more specific font sizes may then be set
// on children elements.
// Doing this on the parent <p> elements seems to fix some CSS issues we had
// with too large inner line breaks spacing when the text track element was
// too small, for some reasons.
addClassName(element, "proportional-style");
element.setAttribute("data-proportional-font-size", "1");
// applies to body, div, p, region, span
const paragraphBackgroundColor = style.backgroundColor;
if (isNonEmptyString(paragraphBackgroundColor)) {
element.style.backgroundColor = ttmlColorToCSSColor(paragraphBackgroundColor);
}
// applies to p
const lineHeight = style.lineHeight;
if (isNonEmptyString(lineHeight)) {
applyLineHeight(element, lineHeight);
}
// applies to p
const textAlign = style.textAlign;
if (isNonEmptyString(textAlign)) {
switch (textAlign) {
case "center":
element.style.textAlign = "center";
break;
case "left":
case "start":
// TODO check what start means (difference with left, writing direction?)
element.style.textAlign = "left";
break;
case "right":
case "end":
// TODO check what end means (difference with right, writing direction?)
element.style.textAlign = "right";
break;
}
}
}
/**
* Creates span of text for the given #text element, with the right style.
*
* TODO create text elements as string? Might help performances.
* @param {Element} el - the #text element, which text content should be
* displayed
* @param {Object} style - the style object for the given text
* @param {Boolean} shouldTrimWhiteSpace - True if the space should be
* trimmed.
* @returns {HTMLElement}
*/
function createTextElement(
el: Node,
style: Partial<Record<string, string>>,
shouldTrimWhiteSpace: boolean,
): HTMLElement {
const textElement = document.createElement("span");
let textContent = el.textContent === null ? "" : el.textContent;
if (shouldTrimWhiteSpace) {
// 1. Trim leading and trailing whitespace.
// 2. Collapse multiple spaces into one.
let trimmed = textContent.trim();
trimmed = trimmed.replace(/\s+/g, " ");
textContent = trimmed;
}
const textNode = document.createTextNode(textContent);
textElement.appendChild(textNode);
textElement.className = "rxp-texttrack-span";
applyTextStyle(textElement, style, shouldTrimWhiteSpace);
return textElement;
}
/**
* Generate every text elements to display in a given paragraph.
* @param {Element} paragraph - The <p> tag.
* @param {Array.<Object>} regions
* @param {Array.<Object>} styles
* @param {Object} paragraphStyle - The general style object of the paragraph.
* @param {Boolean} shouldTrimWhiteSpace
* @returns {Array.<HTMLElement>}
*/
function generateTextContent(
paragraph: Element,
regions: IStyleObject[],
styles: IStyleObject[],
paragraphStyle: Partial<Record<string, string>>,
shouldTrimWhiteSpace: boolean,
): HTMLElement[] {
/**
* Recursive function, taking a node in argument and returning the
* corresponding array of HTMLElement in order.
* @param {Node} node - the node in question
* @param {Object} style - the current state of the style for the node.
* /!\ The style object can be mutated, provide a copy of it.
* @param {Array.<Element>} spans - The spans parent of this node.
* @param {Boolean} shouldTrimWhiteSpaceFromParent - True if the space should be
* trimmed by default. From the parent xml:space parameter.
* @returns {Array.<HTMLElement>}
*/
function loop(
node: Node,
style: IStyleList,
spans: Node[],
shouldTrimWhiteSpaceFromParent: boolean,
): HTMLElement[] {
const childNodes = node.childNodes;
const elements: HTMLElement[] = [];
for (let i = 0; i < childNodes.length; i++) {
const currentNode = childNodes[i];
if (currentNode.nodeName === "#text") {
const { backgroundColor } = getStylingAttributes(
["backgroundColor"],
spans,
styles,
regions,
);
if (isNonEmptyString(backgroundColor)) {
style.backgroundColor = backgroundColor;
} else {
style.backgroundColor = "";
}
const el = createTextElement(currentNode, style, shouldTrimWhiteSpaceFromParent);
elements.push(el);
} else if (isLineBreakElement(currentNode)) {
const br = document.createElement("BR");
elements.push(br);
} else if (
isSpanElement(currentNode) &&
currentNode.nodeType === Node.ELEMENT_NODE &&
currentNode.childNodes.length > 0
) {
const spaceAttribute = (currentNode as Element).getAttribute("xml:space");
const shouldTrimWhiteSpaceOnSpan = isNonEmptyString(spaceAttribute)
? spaceAttribute === "default"
: shouldTrimWhiteSpaceFromParent;
// compute the new applyable style
const newStyle = objectAssign(
{},
style,
getStylingAttributes(SPAN_LEVEL_ATTRIBUTES, [currentNode], styles, regions),
);
elements.push(
...loop(
currentNode,
newStyle,
[currentNode, ...spans],
shouldTrimWhiteSpaceOnSpan,
),
);
}
}
return elements;
}
return loop(paragraph, objectAssign({}, paragraphStyle), [], shouldTrimWhiteSpace);
}
/**
* @param {Element} paragraph
* @param {Element} body
* @param {Array.<Object>} regions
* @param {Array.<Object>} styles
* @param {Object} paragraphStyle
* @param {Object}
* @returns {HTMLElement}
*/
export default function createElement(
paragraph: Element,
body: Element | null,
regions: IStyleObject[],
styles: IStyleObject[],
paragraphStyle: IStyleList,
{
cellResolution,
shouldTrimWhiteSpace,
}: {
shouldTrimWhiteSpace: boolean;
cellResolution: { columns: number; rows: number };
},
): HTMLElement {
const divs = getParentDivElements(paragraph);
const parentElement = document.createElement("DIV");
parentElement.className = "rxp-texttrack-region";
parentElement.setAttribute("data-resolution-columns", String(cellResolution.columns));
parentElement.setAttribute("data-resolution-rows", String(cellResolution.rows));
applyGeneralStyle(parentElement, paragraphStyle);
if (body !== null) {
// applies to body, div, p, region, span
const { bodyBackgroundColor } = getStylingAttributes(
["backgroundColor"],
[...divs, body],
styles,
regions,
);
if (isNonEmptyString(bodyBackgroundColor)) {
parentElement.style.backgroundColor = ttmlColorToCSSColor(bodyBackgroundColor);
}
}
const pElement = document.createElement("p");
pElement.className = "rxp-texttrack-p";
applyPStyle(pElement, paragraphStyle);
const textContent = generateTextContent(
paragraph,
regions,
styles,
paragraphStyle,
shouldTrimWhiteSpace,
);
for (let i = 0; i < textContent.length; i++) {
pElement.appendChild(textContent[i]);
}
// NOTE:
// The following code is for the inclusion of div elements. This has no
// advantage for now, and might only with future evolutions.
// (This is only an indication of what the base of the code could look like).
// if (divs.length) {
// let container = parentElement;
// for (let i = divs.length - 1; i >= 0; i--) {
// // TODO manage style at div level?
// // They are: visibility, display and backgroundColor
// // All these do not have any difference if applied to the <p> element
// // instead of the div.
// // The advantage might only be for multiple <p> elements dispatched
// // in multiple div Which we do not manage anyway for now.
// const divEl = document.createElement("DIV");
// divEl.className = "rxp-texttrack-div";
// container.appendChild(divEl);
// container = divEl;
// }
// container.appendChild(pElement);
// parentElement.appendChild(container);
// } else {
// parentElement.appendChild(pElement);
// }
parentElement.appendChild(pElement);
return parentElement;
}