From 22efc8e833d6694c9d43e49f0224b9b200a1a875 Mon Sep 17 00:00:00 2001 From: morethanwords Date: Sat, 2 Jan 2021 19:22:11 +0400 Subject: [PATCH] Refactor input range --- src/components/rangeSelector.ts | 140 ++++++++++++++++++ src/components/sidebarLeft/index.ts | 3 + .../sidebarLeft/tabs/generalSettings.ts | 90 +++++++++++ src/components/sidebarLeft/tabs/settings.ts | 6 +- src/lib/mediaPlayer.ts | 132 ++--------------- src/scss/partials/_audio.scss | 19 +-- src/scss/partials/_chatBubble.scss | 7 +- src/scss/partials/_ckin.scss | 98 ++++++------ src/scss/partials/_leftSidebar.scss | 40 +++++ 9 files changed, 347 insertions(+), 188 deletions(-) create mode 100644 src/components/rangeSelector.ts create mode 100644 src/components/sidebarLeft/tabs/generalSettings.ts diff --git a/src/components/rangeSelector.ts b/src/components/rangeSelector.ts new file mode 100644 index 00000000..41d5390f --- /dev/null +++ b/src/components/rangeSelector.ts @@ -0,0 +1,140 @@ +import { isTouchSupported } from "../helpers/touchSupport"; +import { clamp } from "../helpers/number"; + +type SUPEREVENT = MouseEvent | TouchEvent; + +export default class RangeSelector { + public container: HTMLDivElement; + protected filled: HTMLDivElement; + protected seek: HTMLInputElement; + + public mousedown = false; + + private events: Partial<{ + //onMouseMove: ProgressLine['onMouseMove'], + onMouseDown: RangeSelector['onMouseDown'], + onMouseUp: RangeSelector['onMouseUp'], + onScrub: (scrubTime: number) => void + }> = {}; + + protected decimals: number; + + constructor(protected step: number, protected value: number, protected min: number, protected max: number) { + this.container = document.createElement('div'); + this.container.classList.add('progress-line'); + + this.filled = document.createElement('div'); + this.filled.classList.add('progress-line__filled'); + + const seek = this.seek = document.createElement('input'); + seek.classList.add('progress-line__seek'); + //seek.setAttribute('max', '0'); + seek.type = 'range'; + seek.step = '' + step; + seek.min = '' + this.min; + seek.max = '' + this.max; + seek.value = '' + value; + + /* this.seek.addEventListener('change', (e) => { + console.log('seek change', e); + }); */ + + if(value) { + this.setProgress(value); + } + + const stepStr = '' + this.step; + const index = stepStr.indexOf('.'); + this.decimals = index === -1 ? 0 : stepStr.length - index - 1; + + //this.setListeners(); + + this.container.append(this.filled, seek); + } + + public setHandlers(events: RangeSelector['events']) { + this.events = events; + } + + onMouseMove = (e: SUPEREVENT) => { + this.mousedown && this.scrub(e); + }; + + onMouseDown = (e: SUPEREVENT) => { + this.scrub(e); + this.mousedown = true; + this.events?.onMouseDown && this.events.onMouseDown(e); + }; + + onMouseUp = (e: SUPEREVENT) => { + this.mousedown = false; + this.events?.onMouseUp && this.events.onMouseUp(e); + }; + + public setListeners() { + this.container.addEventListener('mousemove', this.onMouseMove); + this.container.addEventListener('mousedown', this.onMouseDown); + this.container.addEventListener('mouseup', this.onMouseUp); + + if(isTouchSupported) { + this.container.addEventListener('touchmove', this.onMouseMove, {passive: true}); + this.container.addEventListener('touchstart', this.onMouseDown, {passive: true}); + this.container.addEventListener('touchend', this.onMouseUp, {passive: true}); + } + } + + public setProgress(value: number) { + this.setFilled(value); + this.seek.value = '' + value; + } + + public setFilled(value: number) { + let percents = (value - this.min) / (this.max - this.min); + percents = clamp(percents, 0, 1); + //console.log('setFilled', percents, value); + this.filled.style.width = (percents * 100) + '%'; + //this.filled.style.transform = 'scaleX(' + scaleX + ')'; + } + + protected scrub(e: SUPEREVENT) { + let offsetX: number; + const rect = (e.target as HTMLElement).getBoundingClientRect(); + if(e instanceof MouseEvent) { + offsetX = e.pageX - Math.round(rect.left); + } else { // touch + offsetX = e.targetTouches[0].pageX - Math.round(rect.left); + } + + let value = this.min + (offsetX / Math.round(rect.width) * (this.max - this.min)); + + if((value - this.min) < ((this.max - this.min) / 2)) { + value -= this.step / 10; + } + + //console.log('scrub value:', value, this.decimals, offsetX, rect.width, e); + value = +value.toFixed(this.decimals); + //const dotIndex = ('' + value).indexOf('.'); + //value = +('' + value).slice(0, this.decimals ? dotIndex + this.decimals : dotIndex); + + value = clamp(value, this.min, this.max); + + this.setFilled(value); + + this.events?.onScrub && this.events.onScrub(value); + return value; + } + + public removeListeners() { + this.container.removeEventListener('mousemove', this.onMouseMove); + this.container.removeEventListener('mousedown', this.onMouseDown); + this.container.removeEventListener('mouseup', this.onMouseUp); + + if(isTouchSupported) { + this.container.removeEventListener('touchmove', this.onMouseMove); + this.container.removeEventListener('touchstart', this.onMouseDown); + this.container.removeEventListener('touchend', this.onMouseUp); + } + + this.events = {}; + } +} \ No newline at end of file diff --git a/src/components/sidebarLeft/index.ts b/src/components/sidebarLeft/index.ts index 99850dc9..d7a0f71d 100644 --- a/src/components/sidebarLeft/index.ts +++ b/src/components/sidebarLeft/index.ts @@ -28,6 +28,7 @@ import appMessagesManager from "../../lib/appManagers/appMessagesManager"; import apiManagerProxy from "../../lib/mtproto/mtprotoworker"; import AppSearchSuper from "../appSearchSuper."; import { DateData, fillTipDates } from "../../helpers/date"; +import AppGeneralSettingsTab from "./tabs/generalSettings"; const addMembersTab = new AppAddMembersTab(); const contactsTab = new AppContactsTab(); @@ -73,6 +74,7 @@ export class AppSidebarLeft extends SidebarSlider { public chatFoldersTab: AppChatFoldersTab; public editFolderTab: AppEditFolderTab; public includedChatsTab: AppIncludedChatsTab; + public generalSettingsTab: AppGeneralSettingsTab; //private log = logger('SL'); @@ -107,6 +109,7 @@ export class AppSidebarLeft extends SidebarSlider { this.editFolderTab = new AppEditFolderTab(this); this.includedChatsTab = new AppIncludedChatsTab(this); this.editProfileTab = new AppEditProfileTab(this); + this.generalSettingsTab = new AppGeneralSettingsTab(this); this.menuEl = this.toolsBtn.querySelector('.btn-menu'); this.newBtnMenu = this.sidebarEl.querySelector('#new-menu'); diff --git a/src/components/sidebarLeft/tabs/generalSettings.ts b/src/components/sidebarLeft/tabs/generalSettings.ts new file mode 100644 index 00000000..60cd01f2 --- /dev/null +++ b/src/components/sidebarLeft/tabs/generalSettings.ts @@ -0,0 +1,90 @@ +import { SliderSuperTab } from "../../slider" +import { AppSidebarLeft } from ".."; +import RangeSelector from "../../rangeSelector"; +import { clamp } from "../../../helpers/number"; + +export class RangeSettingSelector { + public container: HTMLDivElement; + private range: RangeSelector; + + constructor(name: string, step: number, initialValue: number, minValue: number, maxValue: number) { + const BASE_CLASS = 'range-setting-selector'; + this.container = document.createElement('div'); + this.container.classList.add(BASE_CLASS); + + const details = document.createElement('div'); + details.classList.add(BASE_CLASS + '-details'); + + const nameDiv = document.createElement('div'); + nameDiv.classList.add(BASE_CLASS + '-name'); + nameDiv.innerHTML = name; + + const valueDiv = document.createElement('div'); + valueDiv.classList.add(BASE_CLASS + '-value'); + valueDiv.innerHTML = '' + initialValue; + + details.append(nameDiv, valueDiv); + + this.range = new RangeSelector(step, initialValue, minValue, maxValue); + this.range.setListeners(); + this.range.setHandlers({ + onScrub: value => { + //console.log('font size scrub:', value); + valueDiv.innerText = '' + value; + } + }); + + this.container.append(details, this.range.container); + } +} + +export default class AppGeneralSettingsTab extends SliderSuperTab { + constructor(appSidebarLeft: AppSidebarLeft) { + super(appSidebarLeft); + } + + init() { + this.container.classList.add('general-settings-container'); + this.title.innerText = 'General'; + + const generateSection = (name: string) => { + const container = document.createElement('div'); + container.classList.add('sidebar-left-section'); + + const hr = document.createElement('hr'); + const h2 = document.createElement('div'); + h2.classList.add('sidebar-left-section-name'); + h2.innerHTML = name; + + const content = document.createElement('div'); + content.classList.add('sidebar-left-section-content'); + content.append(h2); + + container.append(hr, content); + + this.scrollable.append(container); + + return content; + }; + + { + const container = generateSection('Settings'); + + const range = new RangeSettingSelector('Message Text Size', 1, 16, 12, 20); + + container.append(range.container); + } + + generateSection('Keyboard'); + generateSection('Auto-Download Media'); + generateSection('Auto-Play Media'); + generateSection('Stickers'); + } + + onOpen() { + if(this.init) { + this.init(); + this.init = null; + } + } +} \ No newline at end of file diff --git a/src/components/sidebarLeft/tabs/settings.ts b/src/components/sidebarLeft/tabs/settings.ts index 8110e11e..a478ade9 100644 --- a/src/components/sidebarLeft/tabs/settings.ts +++ b/src/components/sidebarLeft/tabs/settings.ts @@ -54,7 +54,7 @@ export default class AppSettingsTab extends SliderSuperTab { const className = 'profile-button btn-primary btn-transparent'; buttonsDiv.append(this.buttons.edit = Button(className, {icon: 'edit', rippleSquare: true, text: 'Edit Profile'})); buttonsDiv.append(this.buttons.folders = Button(className, {icon: 'folder', rippleSquare: true, text: 'Chat Folders'})); - buttonsDiv.append(this.buttons.general = Button(className + ' btn-disabled', {icon: 'settings', rippleSquare: true, text: 'General Settings'})); + buttonsDiv.append(this.buttons.general = Button(className, {icon: 'settings', rippleSquare: true, text: 'General Settings'})); buttonsDiv.append(this.buttons.notifications = Button(className + ' btn-disabled', {icon: 'unmute', rippleSquare: true, text: 'Notifications'})); buttonsDiv.append(this.buttons.privacy = Button(className + ' btn-disabled', {icon: 'lock', rippleSquare: true, text: 'Privacy and Security'})); buttonsDiv.append(this.buttons.language = Button(className + ' btn-disabled', {icon: 'language', rippleSquare: true, text: 'Language'})); @@ -74,6 +74,10 @@ export default class AppSettingsTab extends SliderSuperTab { this.buttons.folders.addEventListener('click', () => { appSidebarLeft.chatFoldersTab.open(); }); + + this.buttons.general.addEventListener('click', () => { + appSidebarLeft.generalSettingsTab.open(); + }); } public fillElements() { diff --git a/src/lib/mediaPlayer.ts b/src/lib/mediaPlayer.ts index 5d92bc0f..278d7a8c 100644 --- a/src/lib/mediaPlayer.ts +++ b/src/lib/mediaPlayer.ts @@ -2,131 +2,18 @@ import { cancelEvent } from "../helpers/dom"; import appMediaPlaybackController from "../components/appMediaPlaybackController"; import { isAppleMobile } from "../helpers/userAgent"; import { isTouchSupported } from "../helpers/touchSupport"; +import RangeSelector from "../components/rangeSelector"; type SUPEREVENT = MouseEvent | TouchEvent; -export class ProgressLine { - public container: HTMLDivElement; - protected filled: HTMLDivElement; - protected seek: HTMLInputElement; - - protected duration = 1; - public mousedown = false; - - private events: Partial<{ - //onMouseMove: ProgressLine['onMouseMove'], - onMouseDown: ProgressLine['onMouseDown'], - onMouseUp: ProgressLine['onMouseUp'], - onScrub: (scrubTime: number) => void - }> = {}; - - constructor(initialValue = 0) { - this.container = document.createElement('div'); - this.container.classList.add('progress-line'); - - this.filled = document.createElement('div'); - this.filled.classList.add('progress-line__filled'); - - const seek = this.seek = document.createElement('input'); - seek.classList.add('progress-line__seek'); - seek.value = '' + initialValue; - seek.setAttribute('min', '0'); - //seek.setAttribute('max', '0'); - seek.type = 'range'; - seek.step = '0.1'; - seek.max = '' + (this.duration * 1000); - - if(initialValue > 0) { - this.setProgress(initialValue); - } - - //this.setListeners(); - - this.container.append(this.filled, seek); - } - - public setHandlers(events: ProgressLine['events']) { - this.events = events; - } - - onMouseMove = (e: SUPEREVENT) => { - this.mousedown && this.scrub(e); - }; - - onMouseDown = (e: SUPEREVENT) => { - this.scrub(e); - this.mousedown = true; - this.events?.onMouseDown && this.events.onMouseDown(e); - }; - - onMouseUp = (e: SUPEREVENT) => { - this.mousedown = false; - this.events?.onMouseUp && this.events.onMouseUp(e); - }; - - public setListeners() { - this.container.addEventListener('mousemove', this.onMouseMove); - this.container.addEventListener('mousedown', this.onMouseDown); - this.container.addEventListener('mouseup', this.onMouseUp); - - if(isTouchSupported) { - this.container.addEventListener('touchmove', this.onMouseMove, {passive: true}); - this.container.addEventListener('touchstart', this.onMouseDown, {passive: true}); - this.container.addEventListener('touchend', this.onMouseUp, {passive: true}); - } - } - - public setProgress(scrubTime: number) { - this.setFilled(scrubTime); - this.seek.value = '' + (scrubTime * 1000); - } - - public setFilled(scrubTime: number) { - let scaleX = scrubTime / this.duration; - scaleX = Math.max(0, Math.min(1, scaleX)); - this.filled.style.transform = 'scaleX(' + scaleX + ')'; - } - - protected scrub(e: SUPEREVENT) { - let offsetX: number; - if(e instanceof MouseEvent) { - offsetX = e.offsetX; - } else { // touch - const rect = (e.target as HTMLElement).getBoundingClientRect(); - offsetX = e.targetTouches[0].pageX - rect.left; - } - - const scrubTime = offsetX / this.container.offsetWidth * this.duration; - - this.setFilled(scrubTime); - - this.events?.onScrub && this.events.onScrub(scrubTime); - return scrubTime; - } - - public removeListeners() { - this.container.removeEventListener('mousemove', this.onMouseMove); - this.container.removeEventListener('mousedown', this.onMouseDown); - this.container.removeEventListener('mouseup', this.onMouseUp); - - if(isTouchSupported) { - this.container.removeEventListener('touchmove', this.onMouseMove); - this.container.removeEventListener('touchstart', this.onMouseDown); - this.container.removeEventListener('touchend', this.onMouseUp); - } - - this.events = {}; - } -} - -export class MediaProgressLine extends ProgressLine { +export class MediaProgressLine extends RangeSelector { private filledLoad: HTMLDivElement; private stopAndScrubTimeout = 0; private progressRAF = 0; constructor(private media: HTMLAudioElement | HTMLVideoElement, private streamable = false) { - super(); + super(1000 / 60 / 1000, 0, 0, 1); if(streamable) { this.filledLoad = document.createElement('div'); @@ -170,8 +57,8 @@ export class MediaProgressLine extends ProgressLine { } onLoadedData = () => { - this.duration = this.media.duration; - this.seek.setAttribute('max', '' + this.duration * 1000); + this.max = this.media.duration; + this.seek.setAttribute('max', '' + this.max); }; onEnded = () => { @@ -226,12 +113,13 @@ export class MediaProgressLine extends ProgressLine { //console.log('onProgress correct range:', nearestStart, end, this.media); const percents = this.media.duration ? end / this.media.duration : 0; - this.filledLoad.style.transform = 'scaleX(' + percents + ')'; + this.filledLoad.style.width = (percents * 100) + '%'; + //this.filledLoad.style.transform = 'scaleX(' + percents + ')'; } protected setSeekMax() { - this.duration = this.media.duration; - if(this.duration > 0) { + this.max = this.media.duration; + if(this.max > 0) { this.onLoadedData(); } else { this.media.addEventListener('loadeddata', this.onLoadedData); @@ -342,7 +230,7 @@ export default class VideoPlayer { video.muted = !video.muted; }); - const volumeProgress = new ProgressLine(); + const volumeProgress = new RangeSelector(0.01, 1, 0, 1); volumeProgress.setListeners(); volumeProgress.setHandlers({ onScrub: currentTime => { diff --git a/src/scss/partials/_audio.scss b/src/scss/partials/_audio.scss index e7835c1e..5020d8e5 100644 --- a/src/scss/partials/_audio.scss +++ b/src/scss/partials/_audio.scss @@ -446,30 +446,21 @@ // * for audio .progress-line { + --height: 2px; + --border-radius: 4px; flex: 1 1 auto; margin-left: 5px; margin-top: -1px; - &__filled { - background-color: #0089ff; - transform-origin: left; - height: 2px; - } - &__loaded { background-color: #cacaca; } &__seek { - height: 2px; + --thumb-size: 12px; + --thumb-color: #0089ff; //background-color: #e6ecf0; - background: rgba(193, 207, 220, 0.39); - - &::-webkit-slider-thumb { - height: 12px; - width: 12px; - border: none; - } + background-color: rgba(193, 207, 220, .39); } } } \ No newline at end of file diff --git a/src/scss/partials/_chatBubble.scss b/src/scss/partials/_chatBubble.scss index 59d62e9f..badae539 100644 --- a/src/scss/partials/_chatBubble.scss +++ b/src/scss/partials/_chatBubble.scss @@ -1964,11 +1964,8 @@ $bubble-margin: .25rem; } &__seek { - background: rgba(124, 195, 107, .52); - - &::-webkit-slider-thumb { - background: #47aa41; - } + --thumb-color: #47aa41; + background-color: rgba(124, 195, 107, .52); } } } diff --git a/src/scss/partials/_ckin.scss b/src/scss/partials/_ckin.scss index 1b477b3d..b150f645 100644 --- a/src/scss/partials/_ckin.scss +++ b/src/scss/partials/_ckin.scss @@ -134,28 +134,18 @@ transition: all .3s; text-align: left; direction: ltr; - border-radius: 0 0 5px 5px; z-index: 6; .progress-line { margin: 0 16px; - height: 5px; - background: rgba(255, 255, 255, 0.38); - border-radius: 4px; - overflow: visible; + border-radius: var(--border-radius); &__filled { background: #63a2e3; - transform-origin: left; - border-radius: 4px; - height: 5px; - transform: scaleX(0); } - &__loaded { - background: rgba(255, 255, 255, 0.38); - left: 11px; - width: calc(100% - 11px); + &__loaded, & { + background: rgba(255, 255, 255, .38); } } } @@ -252,12 +242,13 @@ &__filled { background: #fff; } - - input[type=range]::-webkit-slider-thumb { - height: 15px; - width: 15px; - border-radius: 16px; - background: #fff; + + &__seek { + --thumb-size: 15px; + + &::-webkit-slider-thumb { + background: #fff; + } } } @@ -281,16 +272,23 @@ video::-webkit-media-controls-enclosure { } .progress-line { + --height: 5px; + --border-radius: 6px; + height: var(--height); position: relative; cursor: pointer; &__seek { + --thumb-size: 13px; + --thumb-color: #63a2e3; -webkit-appearance: none; -moz-appearance: none; background: transparent; - height: 4.5px; + width: 100%; + height: 100%; cursor: pointer; padding: 0; + margin: 0; outline: none; &:focus { @@ -310,18 +308,39 @@ video::-webkit-media-controls-enclosure { cursor: pointer; border-radius: 1.3px; -webkit-appearance: none; - transition: all 0.4s ease; } &::-webkit-slider-thumb { - height: 15px; - width: 15px; - border-radius: 16px; - background: #63a2e3; + height: var(--thumb-size); + width: var(--thumb-size); + border-radius: 50%; + background-color: var(--thumb-color); + cursor: pointer; + -webkit-appearance: none; + border: none; + //margin-left: -.5px; + } + + &::-moz-range-thumb { + height: var(--thumb-size); + width: var(--thumb-size); + border-radius: 50%; + background-color: var(--thumb-color); + cursor: pointer; + -webkit-appearance: none; + border: none; + //margin-left: -.5px; + } + + &::-ms-thumb { + height: var(--thumb-size); + width: var(--thumb-size); + border-radius: 50%; + background-color: var(--thumb-color); cursor: pointer; -webkit-appearance: none; - margin-left: -1px; border: none; + //margin-left: -.5px; } &::-moz-range-track { @@ -330,33 +349,20 @@ video::-webkit-media-controls-enclosure { cursor: pointer; border: 1px solid transparent; background: transparent; - border-radius: 1.3px; - } - - &::-moz-range-thumb { - height: 14px; - width: 14px; - border-radius: 50px; - border: 1px solid #63a2e3; - background: #63a2e3; - cursor: pointer; - margin-top: 5px; + //border-radius: 1.3px; } } - &__seek { - position: absolute; - top: 0; - width: 100%; - cursor: pointer; - margin: 0; + &__filled { + padding-right: 1px; // * need because there is border-radius + max-width: 100%; } - &__loaded { + &__seek, &__filled, &__loaded { + border-radius: var(--border-radius); position: absolute; - left: 12px; + height: 100%; top: 0; - width: calc(100% - 12px); } } diff --git a/src/scss/partials/_leftSidebar.scss b/src/scss/partials/_leftSidebar.scss index 235ad997..0e88338c 100644 --- a/src/scss/partials/_leftSidebar.scss +++ b/src/scss/partials/_leftSidebar.scss @@ -878,4 +878,44 @@ .search-group-recent.search-group.search-group-contacts { padding: 0px 0 7px; } +} + +.sidebar-left-section { + padding: .5rem 0 1rem; + + &-content { + padding: 0 1.5rem; + + @include respond-to(handhelds) { + padding: 0 1rem; + } + } + + &-name { + color: #707579; + font-size: 15px; + font-weight: 500; + } +} + +.range-setting-selector { + &-details { + display: flex; + justify-content: space-between; + } + + .progress-line { + --height: 2px; + --border-radius: 4px; + background-color: #e6ecf0; + + &__filled { + background-color: #3390ec; + } + + &__seek { + --thumb-color: #3390ec; + --thumb-size: 12px; + } + } } \ No newline at end of file