mirror of
https://github.com/videojs/video.js.git
synced 2025-02-02 11:34:50 +02:00
refactor(texttracksettings): DRYer code and remove massive HTML blob (#3679)
* DRYer code while keeping tests passing * Replace massive HTML blob with DOM methods * Create obj util and implement it
This commit is contained in:
parent
74cddcad73
commit
fb74c71ba6
@ -1,168 +1,220 @@
|
||||
/**
|
||||
* @file text-track-settings.js
|
||||
*/
|
||||
import Component from '../component';
|
||||
import * as Events from '../utils/events.js';
|
||||
import * as Fn from '../utils/fn.js';
|
||||
import log from '../utils/log.js';
|
||||
import safeParseTuple from 'safe-json-parse/tuple';
|
||||
import window from 'global/window';
|
||||
import Component from '../component';
|
||||
import {createEl} from '../utils/dom';
|
||||
import * as Fn from '../utils/fn';
|
||||
import * as Obj from '../utils/obj';
|
||||
import log from '../utils/log';
|
||||
|
||||
function captionOptionsMenuTemplate(uniqueId, dialogLabelId, dialogDescriptionId) {
|
||||
const template = `
|
||||
<div role="document">
|
||||
<div role="heading" aria-level="1" id="${dialogLabelId}" class="vjs-control-text">Captions Settings Dialog</div>
|
||||
<div id="${dialogDescriptionId}" class="vjs-control-text">Beginning of dialog window. Escape will cancel and close the window.</div>
|
||||
<div class="vjs-tracksettings">
|
||||
<div class="vjs-tracksettings-colors">
|
||||
<fieldset class="vjs-fg-color vjs-tracksetting">
|
||||
<legend>Text</legend>
|
||||
<label class="vjs-label" for="captions-foreground-color-${uniqueId}">Color</label>
|
||||
<select id="captions-foreground-color-${uniqueId}">
|
||||
<option value="#FFF" selected>White</option>
|
||||
<option value="#000">Black</option>
|
||||
<option value="#F00">Red</option>
|
||||
<option value="#0F0">Green</option>
|
||||
<option value="#00F">Blue</option>
|
||||
<option value="#FF0">Yellow</option>
|
||||
<option value="#F0F">Magenta</option>
|
||||
<option value="#0FF">Cyan</option>
|
||||
</select>
|
||||
<span class="vjs-text-opacity vjs-opacity">
|
||||
<label class="vjs-label" for="captions-foreground-opacity-${uniqueId}">Transparency</label>
|
||||
<select id="captions-foreground-opacity-${uniqueId}">
|
||||
<option value="1" selected>Opaque</option>
|
||||
<option value="0.5">Semi-Opaque</option>
|
||||
</select>
|
||||
</span>
|
||||
</fieldset>
|
||||
<fieldset class="vjs-bg-color vjs-tracksetting">
|
||||
<legend>Background</legend>
|
||||
<label class="vjs-label" for="captions-background-color-${uniqueId}">Color</label>
|
||||
<select id="captions-background-color-${uniqueId}">
|
||||
<option value="#000" selected>Black</option>
|
||||
<option value="#FFF">White</option>
|
||||
<option value="#F00">Red</option>
|
||||
<option value="#0F0">Green</option>
|
||||
<option value="#00F">Blue</option>
|
||||
<option value="#FF0">Yellow</option>
|
||||
<option value="#F0F">Magenta</option>
|
||||
<option value="#0FF">Cyan</option>
|
||||
</select>
|
||||
<span class="vjs-bg-opacity vjs-opacity">
|
||||
<label class="vjs-label" for="captions-background-opacity-${uniqueId}">Transparency</label>
|
||||
<select id="captions-background-opacity-${uniqueId}">
|
||||
<option value="1" selected>Opaque</option>
|
||||
<option value="0.5">Semi-Transparent</option>
|
||||
<option value="0">Transparent</option>
|
||||
</select>
|
||||
</span>
|
||||
</fieldset>
|
||||
<fieldset class="window-color vjs-tracksetting">
|
||||
<legend>Window</legend>
|
||||
<label class="vjs-label" for="captions-window-color-${uniqueId}">Color</label>
|
||||
<select id="captions-window-color-${uniqueId}">
|
||||
<option value="#000" selected>Black</option>
|
||||
<option value="#FFF">White</option>
|
||||
<option value="#F00">Red</option>
|
||||
<option value="#0F0">Green</option>
|
||||
<option value="#00F">Blue</option>
|
||||
<option value="#FF0">Yellow</option>
|
||||
<option value="#F0F">Magenta</option>
|
||||
<option value="#0FF">Cyan</option>
|
||||
</select>
|
||||
<span class="vjs-window-opacity vjs-opacity">
|
||||
<label class="vjs-label" for="captions-window-opacity-${uniqueId}">Transparency</label>
|
||||
<select id="captions-window-opacity-${uniqueId}">
|
||||
<option value="0" selected>Transparent</option>
|
||||
<option value="0.5">Semi-Transparent</option>
|
||||
<option value="1">Opaque</option>
|
||||
</select>
|
||||
</span>
|
||||
</fieldset>
|
||||
</div> <!-- vjs-tracksettings-colors -->
|
||||
<div class="vjs-tracksettings-font">
|
||||
<div class="vjs-font-percent vjs-tracksetting">
|
||||
<label class="vjs-label" for="captions-font-size-${uniqueId}">Font Size</label>
|
||||
<select id="captions-font-size-${uniqueId}">
|
||||
<option value="0.50">50%</option>
|
||||
<option value="0.75">75%</option>
|
||||
<option value="1.00" selected>100%</option>
|
||||
<option value="1.25">125%</option>
|
||||
<option value="1.50">150%</option>
|
||||
<option value="1.75">175%</option>
|
||||
<option value="2.00">200%</option>
|
||||
<option value="3.00">300%</option>
|
||||
<option value="4.00">400%</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="vjs-edge-style vjs-tracksetting">
|
||||
<label class="vjs-label" for="captions-edge-style-${uniqueId}">Text Edge Style</label>
|
||||
<select id="captions-edge-style-${uniqueId}">
|
||||
<option value="none" selected>None</option>
|
||||
<option value="raised">Raised</option>
|
||||
<option value="depressed">Depressed</option>
|
||||
<option value="uniform">Uniform</option>
|
||||
<option value="dropshadow">Dropshadow</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="vjs-font-family vjs-tracksetting">
|
||||
<label class="vjs-label" for="captions-font-family-${uniqueId}">Font Family</label>
|
||||
<select id="captions-font-family-${uniqueId}">
|
||||
<option value="proportionalSansSerif" selected>Proportional Sans-Serif</option>
|
||||
<option value="monospaceSansSerif">Monospace Sans-Serif</option>
|
||||
<option value="proportionalSerif">Proportional Serif</option>
|
||||
<option value="monospaceSerif">Monospace Serif</option>
|
||||
<option value="casual">Casual</option>
|
||||
<option value="script">Script</option>
|
||||
<option value="small-caps">Small Caps</option>
|
||||
</select>
|
||||
</div>
|
||||
</div> <!-- vjs-tracksettings-font -->
|
||||
<div class="vjs-tracksettings-controls">
|
||||
<button class="vjs-default-button">Defaults</button>
|
||||
<button class="vjs-done-button">Done</button>
|
||||
</div>
|
||||
</div> <!-- vjs-tracksettings -->
|
||||
</div> <!-- role="document" -->
|
||||
`;
|
||||
const LOCAL_STORAGE_KEY = 'vjs-text-track-settings';
|
||||
|
||||
return template;
|
||||
}
|
||||
const COLOR_BLACK = ['#000', 'Black'];
|
||||
const COLOR_BLUE = ['#00F', 'Blue'];
|
||||
const COLOR_CYAN = ['#0FF', 'Cyan'];
|
||||
const COLOR_GREEN = ['#0F0', 'Green'];
|
||||
const COLOR_MAGENTA = ['#F0F', 'Magenta'];
|
||||
const COLOR_RED = ['#F00', 'Red'];
|
||||
const COLOR_WHITE = ['#FFF', 'White'];
|
||||
const COLOR_YELLOW = ['#FF0', 'Yellow'];
|
||||
|
||||
function getSelectedOptionValue(target) {
|
||||
let selectedOption;
|
||||
const OPACITY_OPAQUE = ['1', 'Opaque'];
|
||||
const OPACITY_SEMI = ['0.5', 'Semi-Transparent'];
|
||||
const OPACITY_TRANS = ['0', 'Transparent'];
|
||||
|
||||
// not all browsers support selectedOptions, so, fallback to options
|
||||
if (target.selectedOptions) {
|
||||
selectedOption = target.selectedOptions[0];
|
||||
} else if (target.options) {
|
||||
selectedOption = target.options[target.options.selectedIndex];
|
||||
// Configuration for the various <select> elements in the DOM of this component.
|
||||
//
|
||||
// Possible keys include:
|
||||
//
|
||||
// `default`:
|
||||
// The default option index. Only needs to be provided if not zero.
|
||||
// `parser`:
|
||||
// A function which is used to parse the value from the selected option in
|
||||
// a customized way.
|
||||
// `selector`:
|
||||
// The selector used to find the associated <select> element.
|
||||
const selectConfigs = {
|
||||
backgroundColor: {
|
||||
selector: '.vjs-bg-color > select',
|
||||
id: 'captions-background-color-%s',
|
||||
label: 'Color',
|
||||
options: [
|
||||
COLOR_BLACK,
|
||||
COLOR_WHITE,
|
||||
COLOR_RED,
|
||||
COLOR_GREEN,
|
||||
COLOR_BLUE,
|
||||
COLOR_YELLOW,
|
||||
COLOR_MAGENTA,
|
||||
COLOR_CYAN
|
||||
]
|
||||
},
|
||||
|
||||
backgroundOpacity: {
|
||||
selector: '.vjs-bg-opacity > select',
|
||||
id: 'captions-background-opacity-%s',
|
||||
label: 'Transparency',
|
||||
options: [
|
||||
OPACITY_OPAQUE,
|
||||
OPACITY_SEMI,
|
||||
OPACITY_TRANS
|
||||
]
|
||||
},
|
||||
|
||||
color: {
|
||||
selector: '.vjs-fg-color > select',
|
||||
id: 'captions-foreground-color-%s',
|
||||
label: 'Color',
|
||||
options: [
|
||||
COLOR_WHITE,
|
||||
COLOR_BLACK,
|
||||
COLOR_RED,
|
||||
COLOR_GREEN,
|
||||
COLOR_BLUE,
|
||||
COLOR_YELLOW,
|
||||
COLOR_MAGENTA,
|
||||
COLOR_CYAN
|
||||
]
|
||||
},
|
||||
|
||||
edgeStyle: {
|
||||
selector: '.vjs-edge-style > select',
|
||||
id: '%s',
|
||||
label: 'Text Edge Style',
|
||||
options: [
|
||||
['none', 'None'],
|
||||
['raised', 'Raised'],
|
||||
['depressed', 'Depressed'],
|
||||
['uniform', 'Uniform'],
|
||||
['dropshadow', 'Dropshadow']
|
||||
]
|
||||
},
|
||||
|
||||
fontFamily: {
|
||||
selector: '.vjs-font-family > select',
|
||||
id: 'captions-font-family-%s',
|
||||
label: 'Font Family',
|
||||
options: [
|
||||
['proportionalSansSerif', 'Proportional Sans-Serif'],
|
||||
['monospaceSansSerif', 'Monospace Sans-Serif'],
|
||||
['proportionalSerif', 'Proportional Serif'],
|
||||
['monospaceSerif', 'Monospace Serif'],
|
||||
['casual', 'Casual'],
|
||||
['script', 'Script'],
|
||||
['small-caps', 'Small Caps']
|
||||
]
|
||||
},
|
||||
|
||||
fontPercent: {
|
||||
selector: '.vjs-font-percent > select',
|
||||
id: 'captions-font-size-%s',
|
||||
label: 'Font Size',
|
||||
options: [
|
||||
['0.50', '50%'],
|
||||
['0.75', '75%'],
|
||||
['1.00', '100%'],
|
||||
['1.25', '125%'],
|
||||
['1.50', '150%'],
|
||||
['1.75', '175%'],
|
||||
['2.00', '200%'],
|
||||
['3.00', '300%'],
|
||||
['4.00', '400%']
|
||||
],
|
||||
default: 2,
|
||||
parser: (v) => v === '1.00' ? null : Number(v)
|
||||
},
|
||||
|
||||
textOpacity: {
|
||||
selector: '.vjs-text-opacity > select',
|
||||
id: 'captions-foreground-opacity-%s',
|
||||
label: 'Transparency',
|
||||
options: [
|
||||
OPACITY_OPAQUE,
|
||||
OPACITY_SEMI
|
||||
]
|
||||
},
|
||||
|
||||
// Options for this object are defined below.
|
||||
windowColor: {
|
||||
selector: '.vjs-window-color > select',
|
||||
id: 'captions-window-color-%s',
|
||||
label: 'Color'
|
||||
},
|
||||
|
||||
// Options for this object are defined below.
|
||||
windowOpacity: {
|
||||
selector: '.vjs-window-opacity > select',
|
||||
id: 'captions-window-opacity-%s',
|
||||
label: 'Transparency',
|
||||
options: [
|
||||
OPACITY_TRANS,
|
||||
OPACITY_SEMI,
|
||||
OPACITY_OPAQUE
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
selectConfigs.windowColor.options = selectConfigs.backgroundColor.options;
|
||||
|
||||
/**
|
||||
* Parses out option values.
|
||||
*
|
||||
* @private
|
||||
* @param {String} value
|
||||
* @param {Function} [parser]
|
||||
* Optional function to adjust the value.
|
||||
* @return {Mixed}
|
||||
* Will be `undefined` if no value exists (or if given value is "none").
|
||||
*/
|
||||
function parseOptionValue(value, parser) {
|
||||
if (parser) {
|
||||
value = parser(value);
|
||||
}
|
||||
|
||||
return selectedOption.value;
|
||||
if (value && value !== 'none') {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function setSelectedOption(target, value) {
|
||||
/**
|
||||
* Gets the value of the selected <option> element within a <select> element.
|
||||
*
|
||||
* @param {Object} config
|
||||
* @param {Function} [parser]
|
||||
* Optional function to adjust the value.
|
||||
* @return {Mixed}
|
||||
*/
|
||||
function getSelectedOptionValue(el, parser) {
|
||||
const value = el.options[el.options.selectedIndex].value;
|
||||
|
||||
return parseOptionValue(value, parser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the selected <option> element within a <select> element based on a
|
||||
* given value.
|
||||
*
|
||||
* @param {Object} el
|
||||
* @param {String} value
|
||||
* @param {Function} [parser]
|
||||
* Optional function to adjust the value before comparing.
|
||||
*/
|
||||
function setSelectedOption(el, value, parser) {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
let i;
|
||||
|
||||
for (i = 0; i < target.options.length; i++) {
|
||||
const option = target.options[i];
|
||||
|
||||
if (option.value === value) {
|
||||
for (let i = 0; i < el.options.length; i++) {
|
||||
if (parseOptionValue(el.options[i].value, parser) === value) {
|
||||
el.selectedIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
target.selectedIndex = i;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manipulate settings of texttracks
|
||||
* Manipulate settings of text tracks
|
||||
*
|
||||
* @param {Object} player Main Player
|
||||
* @param {Object=} options Object of option names and values
|
||||
@ -173,46 +225,196 @@ class TextTrackSettings extends Component {
|
||||
|
||||
constructor(player, options) {
|
||||
super(player, options);
|
||||
this.setDefaults();
|
||||
this.hide();
|
||||
|
||||
this.updateDisplay = Fn.bind(this, this.updateDisplay);
|
||||
|
||||
// Grab `persistTextTrackSettings` from the player options if not passed in child options
|
||||
if (options.persistTextTrackSettings === undefined) {
|
||||
this.options_.persistTextTrackSettings = this.options_.playerOptions.persistTextTrackSettings;
|
||||
}
|
||||
|
||||
Events.on(this.$('.vjs-done-button'), 'click', Fn.bind(this, function() {
|
||||
this.on(this.$('.vjs-done-button'), 'click', () => {
|
||||
this.saveSettings();
|
||||
this.hide();
|
||||
}));
|
||||
});
|
||||
|
||||
Events.on(this.$('.vjs-default-button'), 'click', Fn.bind(this, function() {
|
||||
this.$('.vjs-fg-color > select').selectedIndex = 0;
|
||||
this.$('.vjs-bg-color > select').selectedIndex = 0;
|
||||
this.$('.window-color > select').selectedIndex = 0;
|
||||
this.$('.vjs-text-opacity > select').selectedIndex = 0;
|
||||
this.$('.vjs-bg-opacity > select').selectedIndex = 0;
|
||||
this.$('.vjs-window-opacity > select').selectedIndex = 0;
|
||||
this.$('.vjs-edge-style select').selectedIndex = 0;
|
||||
this.$('.vjs-font-family select').selectedIndex = 0;
|
||||
this.$('.vjs-font-percent select').selectedIndex = 2;
|
||||
this.on(this.$('.vjs-default-button'), 'click', () => {
|
||||
this.setDefaults();
|
||||
this.updateDisplay();
|
||||
}));
|
||||
});
|
||||
|
||||
Events.on(this.$('.vjs-fg-color > select'), 'change', Fn.bind(this, this.updateDisplay));
|
||||
Events.on(this.$('.vjs-bg-color > select'), 'change', Fn.bind(this, this.updateDisplay));
|
||||
Events.on(this.$('.window-color > select'), 'change', Fn.bind(this, this.updateDisplay));
|
||||
Events.on(this.$('.vjs-text-opacity > select'), 'change', Fn.bind(this, this.updateDisplay));
|
||||
Events.on(this.$('.vjs-bg-opacity > select'), 'change', Fn.bind(this, this.updateDisplay));
|
||||
Events.on(this.$('.vjs-window-opacity > select'), 'change', Fn.bind(this, this.updateDisplay));
|
||||
Events.on(this.$('.vjs-font-percent select'), 'change', Fn.bind(this, this.updateDisplay));
|
||||
Events.on(this.$('.vjs-edge-style select'), 'change', Fn.bind(this, this.updateDisplay));
|
||||
Events.on(this.$('.vjs-font-family select'), 'change', Fn.bind(this, this.updateDisplay));
|
||||
Obj.each(selectConfigs, config => {
|
||||
this.on(this.$(config.selector), 'change', this.updateDisplay);
|
||||
});
|
||||
|
||||
if (this.options_.persistTextTrackSettings) {
|
||||
this.restoreSettings();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a <select> element with configured options.
|
||||
*
|
||||
* @private
|
||||
* @return {Element}
|
||||
* @method createElSelect_
|
||||
*/
|
||||
createElSelect_(key) {
|
||||
const config = selectConfigs[key];
|
||||
const id = config.id.replace('%s', this.id_);
|
||||
|
||||
return [
|
||||
createEl('label', {
|
||||
className: 'vjs-label',
|
||||
textContent: config.label
|
||||
}, {
|
||||
for: id
|
||||
}),
|
||||
createEl('select', {id}, undefined, config.options.map(o => {
|
||||
return createEl('option', {
|
||||
textContent: this.localize(o[1]),
|
||||
value: o[0]
|
||||
});
|
||||
}))
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create foreground color element for the component
|
||||
*
|
||||
* @private
|
||||
* @return {Element}
|
||||
* @method createElFgColor_
|
||||
*/
|
||||
createElFgColor_() {
|
||||
const legend = createEl('legend', {
|
||||
textContent: this.localize('Text')
|
||||
});
|
||||
|
||||
const select = this.createElSelect_('color');
|
||||
|
||||
const opacity = createEl('span', {
|
||||
className: 'vjs-text-opacity vjs-opacity'
|
||||
}, undefined, this.createElSelect_('textOpacity'));
|
||||
|
||||
return createEl('fieldset', {
|
||||
className: 'vjs-fg-color vjs-tracksetting'
|
||||
}, undefined, [legend].concat(select, opacity));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create background color element for the component
|
||||
*
|
||||
* @private
|
||||
* @return {Element}
|
||||
* @method createElBgColor_
|
||||
*/
|
||||
createElBgColor_() {
|
||||
const legend = createEl('legend', {
|
||||
textContent: this.localize('Background')
|
||||
});
|
||||
|
||||
const select = this.createElSelect_('backgroundColor');
|
||||
|
||||
const opacity = createEl('span', {
|
||||
className: 'vjs-bg-opacity vjs-opacity'
|
||||
}, undefined, this.createElSelect_('backgroundOpacity'));
|
||||
|
||||
return createEl('fieldset', {
|
||||
className: 'vjs-bg-color vjs-tracksetting'
|
||||
}, undefined, [legend].concat(select, opacity));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create window color element for the component
|
||||
*
|
||||
* @private
|
||||
* @return {Element}
|
||||
* @method createElWinColor_
|
||||
*/
|
||||
createElWinColor_() {
|
||||
const legend = createEl('legend', {
|
||||
textContent: this.localize('Window')
|
||||
});
|
||||
|
||||
const select = this.createElSelect_('windowColor');
|
||||
|
||||
const opacity = createEl('span', {
|
||||
className: 'vjs-window-opacity vjs-opacity'
|
||||
}, undefined, this.createElSelect_('windowOpacity'));
|
||||
|
||||
return createEl('fieldset', {
|
||||
className: 'vjs-window-color vjs-tracksetting'
|
||||
}, undefined, [legend].concat(select, opacity));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create color elements for the component
|
||||
*
|
||||
* @private
|
||||
* @return {Element}
|
||||
* @method createElColors_
|
||||
*/
|
||||
createElColors_() {
|
||||
return createEl('div', {
|
||||
className: 'vjs-tracksettings-colors'
|
||||
}, undefined, [
|
||||
this.createElFgColor_(),
|
||||
this.createElBgColor_(),
|
||||
this.createElWinColor_()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create font elements for the component
|
||||
*
|
||||
* @private
|
||||
* @return {Element}
|
||||
* @method createElFont_
|
||||
*/
|
||||
createElFont_() {
|
||||
const fontPercent = createEl('div', {
|
||||
className: 'vjs-font-percent vjs-tracksetting'
|
||||
}, undefined, this.createElSelect_('fontPercent'));
|
||||
|
||||
const edgeStyle = createEl('div', {
|
||||
className: 'vjs-edge-style vjs-tracksetting'
|
||||
}, undefined, this.createElSelect_('edgeStyle'));
|
||||
|
||||
const fontFamily = createEl('div', {
|
||||
className: 'vjs-font-family vjs-tracksetting'
|
||||
}, undefined, this.createElSelect_('fontFamily'));
|
||||
|
||||
return createEl('div', {
|
||||
className: 'vjs-tracksettings-font'
|
||||
}, undefined, [fontPercent, edgeStyle, fontFamily]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create controls for the component
|
||||
*
|
||||
* @private
|
||||
* @return {Element}
|
||||
* @method createElControls_
|
||||
*/
|
||||
createElControls_() {
|
||||
const defaultsButton = createEl('button', {
|
||||
className: 'vjs-default-button',
|
||||
textContent: this.localize('Defaults')
|
||||
});
|
||||
|
||||
const doneButton = createEl('button', {
|
||||
className: 'vjs-done-button',
|
||||
textContent: 'Done'
|
||||
});
|
||||
|
||||
return createEl('div', {
|
||||
className: 'vjs-tracksettings-controls'
|
||||
}, undefined, [defaultsButton, doneButton]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the component's DOM element
|
||||
*
|
||||
@ -220,99 +422,86 @@ class TextTrackSettings extends Component {
|
||||
* @method createEl
|
||||
*/
|
||||
createEl() {
|
||||
const uniqueId = this.id_;
|
||||
const dialogLabelId = 'TTsettingsDialogLabel-' + uniqueId;
|
||||
const dialogDescriptionId = 'TTsettingsDialogDescription-' + uniqueId;
|
||||
const settings = createEl('div', {
|
||||
className: 'vjs-tracksettings'
|
||||
}, undefined, [
|
||||
this.createElColors_(),
|
||||
this.createElFont_(),
|
||||
this.createElControls_()
|
||||
]);
|
||||
|
||||
return super.createEl('div', {
|
||||
const heading = createEl('div', {
|
||||
className: 'vjs-control-text',
|
||||
id: `TTsettingsDialogLabel-${this.id_}`,
|
||||
textContent: 'Caption Settings Dialog'
|
||||
}, {
|
||||
'aria-level': '1',
|
||||
'role': 'heading'
|
||||
});
|
||||
|
||||
const description = createEl('div', {
|
||||
className: 'vjs-control-text',
|
||||
id: `TTsettingsDialogDescription-${this.id_}`,
|
||||
textContent: 'Beginning of dialog window. Escape will cancel and close the window.'
|
||||
});
|
||||
|
||||
const doc = createEl('div', undefined, {
|
||||
role: 'document'
|
||||
}, [heading, description, settings]);
|
||||
|
||||
return createEl('div', {
|
||||
className: 'vjs-caption-settings vjs-modal-overlay',
|
||||
innerHTML: captionOptionsMenuTemplate(uniqueId, dialogLabelId, dialogDescriptionId),
|
||||
tabIndex: -1
|
||||
}, {
|
||||
'role': 'dialog',
|
||||
'aria-labelledby': dialogLabelId,
|
||||
'aria-describedby': dialogDescriptionId
|
||||
'aria-labelledby': heading.id,
|
||||
'aria-describedby': description.id
|
||||
}, doc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an object of text track settings (or null).
|
||||
*
|
||||
* @return {Object}
|
||||
* An object with config values parsed from the DOM or localStorage.
|
||||
* @method getValues
|
||||
*/
|
||||
getValues() {
|
||||
return Obj.reduce(selectConfigs, (accum, config, key) => {
|
||||
const value = getSelectedOptionValue(this.$(config.selector), config.parser);
|
||||
|
||||
if (value !== undefined) {
|
||||
accum[key] = value;
|
||||
}
|
||||
|
||||
return accum;
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets text track settings from an object of values.
|
||||
*
|
||||
* @param {Object} values
|
||||
* An object with config values parsed from the DOM or localStorage.
|
||||
* @method setValues
|
||||
*/
|
||||
setValues(values) {
|
||||
Obj.each(selectConfigs, (config, key) => {
|
||||
setSelectedOption(this.$(config.selector), values[key], config.parser);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get texttrack settings
|
||||
* Settings are
|
||||
* .vjs-edge-style
|
||||
* .vjs-font-family
|
||||
* .vjs-fg-color
|
||||
* .vjs-text-opacity
|
||||
* .vjs-bg-color
|
||||
* .vjs-bg-opacity
|
||||
* .window-color
|
||||
* .vjs-window-opacity
|
||||
* Sets all <select> elements to their default values.
|
||||
*
|
||||
* @return {Object}
|
||||
* @method getValues
|
||||
* @method setDefaults
|
||||
*/
|
||||
getValues() {
|
||||
const textEdge = getSelectedOptionValue(this.$('.vjs-edge-style select'));
|
||||
const fontFamily = getSelectedOptionValue(this.$('.vjs-font-family select'));
|
||||
const fgColor = getSelectedOptionValue(this.$('.vjs-fg-color > select'));
|
||||
const textOpacity = getSelectedOptionValue(this.$('.vjs-text-opacity > select'));
|
||||
const bgColor = getSelectedOptionValue(this.$('.vjs-bg-color > select'));
|
||||
const bgOpacity = getSelectedOptionValue(this.$('.vjs-bg-opacity > select'));
|
||||
const windowColor = getSelectedOptionValue(this.$('.window-color > select'));
|
||||
const windowOpacity = getSelectedOptionValue(this.$('.vjs-window-opacity > select'));
|
||||
const fontPercent = window.parseFloat(getSelectedOptionValue(this.$('.vjs-font-percent > select')));
|
||||
setDefaults() {
|
||||
Obj.each(selectConfigs, (config) => {
|
||||
const index = config.hasOwnProperty('default') ? config.default : 0;
|
||||
|
||||
const result = {
|
||||
fontPercent,
|
||||
fontFamily,
|
||||
textOpacity,
|
||||
windowColor,
|
||||
windowOpacity,
|
||||
backgroundOpacity: bgOpacity,
|
||||
edgeStyle: textEdge,
|
||||
color: fgColor,
|
||||
backgroundColor: bgColor
|
||||
};
|
||||
|
||||
for (const name in result) {
|
||||
if (result[name] === '' || result[name] === 'none' || (name === 'fontPercent' && result[name] === 1.00)) {
|
||||
delete result[name];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set texttrack settings
|
||||
* Settings are
|
||||
* .vjs-edge-style
|
||||
* .vjs-font-family
|
||||
* .vjs-fg-color
|
||||
* .vjs-text-opacity
|
||||
* .vjs-bg-color
|
||||
* .vjs-bg-opacity
|
||||
* .window-color
|
||||
* .vjs-window-opacity
|
||||
*
|
||||
* @param {Object} values Object with texttrack setting values
|
||||
* @method setValues
|
||||
*/
|
||||
setValues(values) {
|
||||
setSelectedOption(this.$('.vjs-edge-style select'), values.edgeStyle);
|
||||
setSelectedOption(this.$('.vjs-font-family select'), values.fontFamily);
|
||||
setSelectedOption(this.$('.vjs-fg-color > select'), values.color);
|
||||
setSelectedOption(this.$('.vjs-text-opacity > select'), values.textOpacity);
|
||||
setSelectedOption(this.$('.vjs-bg-color > select'), values.backgroundColor);
|
||||
setSelectedOption(this.$('.vjs-bg-opacity > select'), values.backgroundOpacity);
|
||||
setSelectedOption(this.$('.window-color > select'), values.windowColor);
|
||||
setSelectedOption(this.$('.vjs-window-opacity > select'), values.windowOpacity);
|
||||
|
||||
let fontPercent = values.fontPercent;
|
||||
|
||||
if (fontPercent) {
|
||||
fontPercent = fontPercent.toFixed(2);
|
||||
}
|
||||
|
||||
setSelectedOption(this.$('.vjs-font-percent > select'), fontPercent);
|
||||
this.$(config.selector).selectedIndex = index;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -321,17 +510,12 @@ class TextTrackSettings extends Component {
|
||||
* @method restoreSettings
|
||||
*/
|
||||
restoreSettings() {
|
||||
let err;
|
||||
let values;
|
||||
|
||||
try {
|
||||
[err, values] = safeParseTuple(window.localStorage.getItem('vjs-text-track-settings'));
|
||||
|
||||
if (err) {
|
||||
log.error(err);
|
||||
}
|
||||
} catch (e) {
|
||||
log.warn(e);
|
||||
values = JSON.parse(window.localStorage.getItem(LOCAL_STORAGE_KEY));
|
||||
} catch (err) {
|
||||
log.warn(err);
|
||||
}
|
||||
|
||||
if (values) {
|
||||
@ -340,7 +524,7 @@ class TextTrackSettings extends Component {
|
||||
}
|
||||
|
||||
/**
|
||||
* Save texttrack settings to local storage
|
||||
* Save text track settings to local storage
|
||||
*
|
||||
* @method saveSettings
|
||||
*/
|
||||
@ -352,18 +536,18 @@ class TextTrackSettings extends Component {
|
||||
const values = this.getValues();
|
||||
|
||||
try {
|
||||
if (Object.getOwnPropertyNames(values).length > 0) {
|
||||
window.localStorage.setItem('vjs-text-track-settings', JSON.stringify(values));
|
||||
if (Object.keys(values).length) {
|
||||
window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(values));
|
||||
} else {
|
||||
window.localStorage.removeItem('vjs-text-track-settings');
|
||||
window.localStorage.removeItem(LOCAL_STORAGE_KEY);
|
||||
}
|
||||
} catch (e) {
|
||||
log.warn(e);
|
||||
} catch (err) {
|
||||
log.warn(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update display of texttrack settings
|
||||
* Update display of text track settings
|
||||
*
|
||||
* @method updateDisplay
|
||||
*/
|
||||
|
@ -96,10 +96,11 @@ export function getEl(id) {
|
||||
* @param {String} [tagName='div'] Name of tag to be created.
|
||||
* @param {Object} [properties={}] Element properties to be applied.
|
||||
* @param {Object} [attributes={}] Element attributes to be applied.
|
||||
* @param {String|Element|TextNode|Array|Function} [content] Contents for the element (see: `normalizeContent`)
|
||||
* @return {Element}
|
||||
* @function createEl
|
||||
*/
|
||||
export function createEl(tagName = 'div', properties = {}, attributes = {}) {
|
||||
export function createEl(tagName = 'div', properties = {}, attributes = {}, content) {
|
||||
const el = document.createElement(tagName);
|
||||
|
||||
Object.getOwnPropertyNames(properties).forEach(function(propName) {
|
||||
@ -113,6 +114,11 @@ export function createEl(tagName = 'div', properties = {}, attributes = {}) {
|
||||
has been deprecated. Use the third argument instead.
|
||||
createEl(type, properties, attributes). Attempting to set ${propName} to ${val}.`);
|
||||
el.setAttribute(propName, val);
|
||||
|
||||
// Handle textContent since it's not supported everywhere and we have a
|
||||
// method for it.
|
||||
} else if (propName === 'textContent') {
|
||||
textContent(el, val);
|
||||
} else {
|
||||
el[propName] = val;
|
||||
}
|
||||
@ -122,6 +128,10 @@ export function createEl(tagName = 'div', properties = {}, attributes = {}) {
|
||||
el.setAttribute(attrName, attributes[attrName]);
|
||||
});
|
||||
|
||||
if (content) {
|
||||
appendContent(el, content);
|
||||
}
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
@ -139,6 +149,7 @@ export function textContent(el, text) {
|
||||
} else {
|
||||
el.textContent = text;
|
||||
}
|
||||
return el;
|
||||
}
|
||||
|
||||
/**
|
||||
|
32
src/js/utils/obj.js
Normal file
32
src/js/utils/obj.js
Normal file
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* @file obj.js
|
||||
*/
|
||||
|
||||
/**
|
||||
* Array-like iteration for objects.
|
||||
*
|
||||
* @param {Object} object
|
||||
* @param {Function} fn
|
||||
* A callback function which is called for each key in the object. It
|
||||
* receives the value and key as arguments.
|
||||
*/
|
||||
export function each(object, fn) {
|
||||
Object.keys(object).forEach(key => fn(object[key], key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Array-like reduce for objects.
|
||||
*
|
||||
* @param {Object} object
|
||||
* @param {Function} fn
|
||||
* A callback function which is called for each key in the object. It
|
||||
* receives the accumulated value and the per-iteration value and key
|
||||
* as arguments.
|
||||
* @param {Mixed} [initial = 0]
|
||||
* @return {Mixed}
|
||||
*/
|
||||
export function reduce(object, fn, initial = 0) {
|
||||
return Object.keys(object).reduce(
|
||||
(accum, key) => fn(accum, object[key], key),
|
||||
initial);
|
||||
}
|
@ -31,6 +31,7 @@ QUnit.test('should update settings', function(assert) {
|
||||
tracks,
|
||||
persistTextTrackSettings: true
|
||||
});
|
||||
|
||||
const newSettings = {
|
||||
backgroundOpacity: '0.5',
|
||||
textOpacity: '0.5',
|
||||
@ -44,43 +45,52 @@ QUnit.test('should update settings', function(assert) {
|
||||
};
|
||||
|
||||
player.textTrackSettings.setValues(newSettings);
|
||||
|
||||
assert.deepEqual(player.textTrackSettings.getValues(),
|
||||
newSettings,
|
||||
'values are updated');
|
||||
newSettings,
|
||||
'values are updated');
|
||||
|
||||
assert.equal(player.$('.vjs-fg-color > select').selectedIndex,
|
||||
2,
|
||||
'fg-color is set to new value');
|
||||
2,
|
||||
'fg-color is set to new value');
|
||||
|
||||
assert.equal(player.$('.vjs-bg-color > select').selectedIndex,
|
||||
1,
|
||||
'bg-color is set to new value');
|
||||
assert.equal(player.$('.window-color > select').selectedIndex,
|
||||
1,
|
||||
'window-color is set to new value');
|
||||
1,
|
||||
'bg-color is set to new value');
|
||||
|
||||
assert.equal(player.$('.vjs-window-color > select').selectedIndex,
|
||||
1,
|
||||
'window-color is set to new value');
|
||||
|
||||
assert.equal(player.$('.vjs-text-opacity > select').selectedIndex,
|
||||
1,
|
||||
'text-opacity is set to new value');
|
||||
1,
|
||||
'text-opacity is set to new value');
|
||||
|
||||
assert.equal(player.$('.vjs-bg-opacity > select').selectedIndex,
|
||||
1,
|
||||
'bg-opacity is set to new value');
|
||||
1,
|
||||
'bg-opacity is set to new value');
|
||||
|
||||
assert.equal(player.$('.vjs-window-opacity > select').selectedIndex,
|
||||
1,
|
||||
'window-opacity is set to new value');
|
||||
1,
|
||||
'window-opacity is set to new value');
|
||||
|
||||
assert.equal(player.$('.vjs-edge-style select').selectedIndex,
|
||||
1,
|
||||
'edge-style is set to new value');
|
||||
1,
|
||||
'edge-style is set to new value');
|
||||
|
||||
assert.equal(player.$('.vjs-font-family select').selectedIndex,
|
||||
3,
|
||||
'font-family is set to new value');
|
||||
3,
|
||||
'font-family is set to new value');
|
||||
|
||||
assert.equal(player.$('.vjs-font-percent select').selectedIndex,
|
||||
3,
|
||||
'font-percent is set to new value');
|
||||
3,
|
||||
'font-percent is set to new value');
|
||||
|
||||
Events.trigger(player.$('.vjs-done-button'), 'click');
|
||||
assert.deepEqual(safeParseTuple(
|
||||
window.localStorage.getItem('vjs-text-track-settings'))[1],
|
||||
newSettings,
|
||||
'values are saved');
|
||||
|
||||
assert.deepEqual(safeParseTuple(window.localStorage.getItem('vjs-text-track-settings'))[1],
|
||||
newSettings,
|
||||
'values are saved');
|
||||
|
||||
player.dispose();
|
||||
});
|
||||
@ -93,7 +103,7 @@ QUnit.test('should restore default settings', function(assert) {
|
||||
|
||||
player.$('.vjs-fg-color > select').selectedIndex = 1;
|
||||
player.$('.vjs-bg-color > select').selectedIndex = 1;
|
||||
player.$('.window-color > select').selectedIndex = 1;
|
||||
player.$('.vjs-window-color > select').selectedIndex = 1;
|
||||
player.$('.vjs-text-opacity > select').selectedIndex = 1;
|
||||
player.$('.vjs-bg-opacity > select').selectedIndex = 1;
|
||||
player.$('.vjs-window-opacity > select').selectedIndex = 1;
|
||||
@ -106,8 +116,8 @@ QUnit.test('should restore default settings', function(assert) {
|
||||
Events.trigger(player.$('.vjs-done-button'), 'click');
|
||||
|
||||
assert.deepEqual(player.textTrackSettings.getValues(),
|
||||
defaultSettings,
|
||||
'values are defaulted');
|
||||
defaultSettings,
|
||||
'values are defaulted');
|
||||
// TODO:
|
||||
// MikeA: need to figure out how to modify saveSettings
|
||||
// to factor in defaults are no longer null
|
||||
@ -116,32 +126,40 @@ QUnit.test('should restore default settings', function(assert) {
|
||||
// 'values are saved');
|
||||
|
||||
assert.equal(player.$('.vjs-fg-color > select').selectedIndex,
|
||||
0,
|
||||
'fg-color is set to default value');
|
||||
0,
|
||||
'fg-color is set to default value');
|
||||
|
||||
assert.equal(player.$('.vjs-bg-color > select').selectedIndex,
|
||||
0,
|
||||
'bg-color is set to default value');
|
||||
assert.equal(player.$('.window-color > select').selectedIndex,
|
||||
0,
|
||||
'window-color is set to default value');
|
||||
0,
|
||||
'bg-color is set to default value');
|
||||
|
||||
assert.equal(player.$('.vjs-window-color > select').selectedIndex,
|
||||
0,
|
||||
'window-color is set to default value');
|
||||
|
||||
assert.equal(player.$('.vjs-text-opacity > select').selectedIndex,
|
||||
0,
|
||||
'text-opacity is set to default value');
|
||||
0,
|
||||
'text-opacity is set to default value');
|
||||
|
||||
assert.equal(player.$('.vjs-bg-opacity > select').selectedIndex,
|
||||
0,
|
||||
'bg-opacity is set to default value');
|
||||
0,
|
||||
'bg-opacity is set to default value');
|
||||
|
||||
assert.equal(player.$('.vjs-window-opacity > select').selectedIndex,
|
||||
0,
|
||||
'window-opacity is set to default value');
|
||||
0,
|
||||
'window-opacity is set to default value');
|
||||
|
||||
assert.equal(player.$('.vjs-edge-style select').selectedIndex,
|
||||
0,
|
||||
'edge-style is set to default value');
|
||||
0,
|
||||
'edge-style is set to default value');
|
||||
|
||||
assert.equal(player.$('.vjs-font-family select').selectedIndex,
|
||||
0,
|
||||
'font-family is set to default value');
|
||||
0,
|
||||
'font-family is set to default value');
|
||||
|
||||
assert.equal(player.$('.vjs-font-percent select').selectedIndex,
|
||||
2,
|
||||
'font-percent is set to default value');
|
||||
2,
|
||||
'font-percent is set to default value');
|
||||
|
||||
player.dispose();
|
||||
});
|
||||
|
@ -15,8 +15,8 @@ QUnit.test('should return the element with the ID', function(assert) {
|
||||
el1.id = 'test_id1';
|
||||
el2.id = 'test_id2';
|
||||
|
||||
assert.ok(Dom.getEl('test_id1') === el1, 'found element for ID');
|
||||
assert.ok(Dom.getEl('#test_id2') === el2, 'found element for CSS ID');
|
||||
assert.strictEqual(Dom.getEl('test_id1'), el1, 'found element for ID');
|
||||
assert.strictEqual(Dom.getEl('#test_id2'), el2, 'found element for CSS ID');
|
||||
});
|
||||
|
||||
QUnit.test('should create an element', function(assert) {
|
||||
@ -27,10 +27,27 @@ QUnit.test('should create an element', function(assert) {
|
||||
'data-test': 'asdf'
|
||||
});
|
||||
|
||||
assert.ok(div.nodeName === 'DIV');
|
||||
assert.ok(span.nodeName === 'SPAN');
|
||||
assert.ok(span.getAttribute('data-test') === 'asdf');
|
||||
assert.ok(span.innerHTML === 'fdsa');
|
||||
assert.strictEqual(div.nodeName, 'DIV');
|
||||
assert.strictEqual(span.nodeName, 'SPAN');
|
||||
assert.strictEqual(span.getAttribute('data-test'), 'asdf');
|
||||
assert.strictEqual(span.innerHTML, 'fdsa');
|
||||
});
|
||||
|
||||
QUnit.test('should create an element, supporting textContent', function(assert) {
|
||||
const span = Dom.createEl('span', {textContent: 'howdy'});
|
||||
|
||||
if (span.textContent) {
|
||||
assert.strictEqual(span.textContent, 'howdy', 'works in browsers that support textContent');
|
||||
} else {
|
||||
assert.strictEqual(span.innerText, 'howdy', 'works in browsers that DO NOT support textContent');
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test('should create an element with content', function(assert) {
|
||||
const span = Dom.createEl('span');
|
||||
const div = Dom.createEl('div', undefined, undefined, span);
|
||||
|
||||
assert.strictEqual(div.firstChild, span);
|
||||
});
|
||||
|
||||
QUnit.test('should insert an element first in another', function(assert) {
|
||||
@ -39,28 +56,28 @@ QUnit.test('should insert an element first in another', function(assert) {
|
||||
const parent = document.createElement('div');
|
||||
|
||||
Dom.insertElFirst(el1, parent);
|
||||
assert.ok(parent.firstChild === el1, 'inserts first into empty parent');
|
||||
assert.strictEqual(parent.firstChild, el1, 'inserts first into empty parent');
|
||||
|
||||
Dom.insertElFirst(el2, parent);
|
||||
assert.ok(parent.firstChild === el2, 'inserts first into parent with child');
|
||||
assert.strictEqual(parent.firstChild, el2, 'inserts first into parent with child');
|
||||
});
|
||||
|
||||
QUnit.test('should get and remove data from an element', function(assert) {
|
||||
const el = document.createElement('div');
|
||||
const data = Dom.getElData(el);
|
||||
|
||||
assert.ok(typeof data === 'object', 'data object created');
|
||||
assert.strictEqual(typeof data, 'object', 'data object created');
|
||||
|
||||
// Add data
|
||||
const testData = { asdf: 'fdsa' };
|
||||
const testData = {asdf: 'fdsa'};
|
||||
|
||||
data.test = testData;
|
||||
assert.ok(Dom.getElData(el).test === testData, 'data added');
|
||||
assert.strictEqual(Dom.getElData(el).test, testData, 'data added');
|
||||
|
||||
// Remove all data
|
||||
Dom.removeElData(el);
|
||||
|
||||
assert.ok(!Dom.hasElData(el), 'cached item emptied');
|
||||
assert.notOk(Dom.hasElData(el), 'cached item emptied');
|
||||
});
|
||||
|
||||
QUnit.test('addElClass()', function(assert) {
|
||||
|
58
test/unit/utils/obj.test.js
Normal file
58
test/unit/utils/obj.test.js
Normal file
@ -0,0 +1,58 @@
|
||||
/* eslint-env qunit */
|
||||
import sinon from 'sinon';
|
||||
import * as Obj from '../../../src/js/utils/obj';
|
||||
|
||||
QUnit.module('utils/obj');
|
||||
|
||||
QUnit.test('each', function(assert) {
|
||||
const spy = sinon.spy();
|
||||
|
||||
Obj.each({
|
||||
a: 1,
|
||||
b: 'foo',
|
||||
c: null
|
||||
}, spy);
|
||||
|
||||
assert.strictEqual(spy.callCount, 3);
|
||||
assert.ok(spy.calledWith(1, 'a'));
|
||||
assert.ok(spy.calledWith('foo', 'b'));
|
||||
assert.ok(spy.calledWith(null, 'c'));
|
||||
|
||||
Obj.each({}, spy);
|
||||
assert.strictEqual(spy.callCount, 3, 'an empty object was not iterated over');
|
||||
});
|
||||
|
||||
QUnit.test('reduce', function(assert) {
|
||||
const first = Obj.reduce({
|
||||
a: 1,
|
||||
b: 2,
|
||||
c: 3,
|
||||
d: 4
|
||||
}, (accum, value) => accum + value);
|
||||
|
||||
assert.strictEqual(first, 10);
|
||||
|
||||
const second = Obj.reduce({
|
||||
a: 1,
|
||||
b: 2,
|
||||
c: 3,
|
||||
d: 4
|
||||
}, (accum, value) => accum + value, 10);
|
||||
|
||||
assert.strictEqual(second, 20);
|
||||
|
||||
const third = Obj.reduce({
|
||||
a: 1,
|
||||
b: 2,
|
||||
c: 3,
|
||||
d: 4
|
||||
}, (accum, value, key) => {
|
||||
accum[key] = 0 - value;
|
||||
return accum;
|
||||
}, {});
|
||||
|
||||
assert.strictEqual(third.a, -1);
|
||||
assert.strictEqual(third.b, -2);
|
||||
assert.strictEqual(third.c, -3);
|
||||
assert.strictEqual(third.d, -4);
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user