Browse Source

New input field:

Support emoji
Support RTL
Support cyrillic
master
Eduard Kuzmenko 4 years ago
parent
commit
a885492a09
  1. 91
      src/components/chat/input.ts
  2. 102
      src/components/inputField.ts
  3. 51
      src/components/popupCreatePoll.ts
  4. 16
      src/components/popupNewMedia.ts
  5. 4
      src/components/scrollable.ts
  6. 2
      src/components/sidebarLeft/tabs/archivedTab.ts
  7. 141
      src/components/sidebarLeft/tabs/editProfile.ts
  8. 22
      src/helpers/dom.ts
  9. 34
      src/index.hbs
  10. 14
      src/lib/appManagers/appChatsManager.ts
  11. 2
      src/lib/appManagers/appUsersManager.ts
  12. 6
      src/lib/richtextprocessor.ts
  13. 19
      src/scss/partials/_button.scss
  14. 9
      src/scss/partials/_chat.scss
  15. 155
      src/scss/partials/_input.scss
  16. 2
      src/scss/partials/pages/_authCode.scss
  17. 9
      src/scss/partials/popups/_createPoll.scss
  18. 15
      src/scss/partials/popups/_mediaAttacher.scss
  19. 167
      src/scss/style.scss

91
src/components/chat/input.ts

@ -11,7 +11,7 @@ import apiManager from "../../lib/mtproto/mtprotoworker";
import opusDecodeController from "../../lib/opusDecodeController"; import opusDecodeController from "../../lib/opusDecodeController";
import { RichTextProcessor } from "../../lib/richtextprocessor"; import { RichTextProcessor } from "../../lib/richtextprocessor";
import $rootScope from '../../lib/rootScope'; import $rootScope from '../../lib/rootScope';
import { cancelEvent, findUpClassName, getRichValue } from "../../helpers/dom"; import { cancelEvent, findUpClassName, getRichValue, isInputEmpty, serializeNodes } from "../../helpers/dom";
import ButtonMenu, { ButtonMenuItemOptions } from '../buttonMenu'; import ButtonMenu, { ButtonMenuItemOptions } from '../buttonMenu';
import emoticonsDropdown from "../emoticonsDropdown"; import emoticonsDropdown from "../emoticonsDropdown";
import PopupCreatePoll from "../popupCreatePoll"; import PopupCreatePoll from "../popupCreatePoll";
@ -21,7 +21,7 @@ import { ripple } from '../ripple';
import Scrollable from "../scrollable"; import Scrollable from "../scrollable";
import { toast } from "../toast"; import { toast } from "../toast";
import { wrapReply } from "../wrappers"; import { wrapReply } from "../wrappers";
import { checkRTL } from '../../helpers/string'; import InputField from '../inputField';
const RECORD_MIN_TIME = 500; const RECORD_MIN_TIME = 500;
const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this group.'; const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this group.';
@ -30,7 +30,7 @@ type ChatInputHelperType = 'edit' | 'webpage' | 'forward' | 'reply';
export class ChatInput { export class ChatInput {
public pageEl = document.getElementById('page-chats') as HTMLDivElement; public pageEl = document.getElementById('page-chats') as HTMLDivElement;
public messageInput = document.getElementById('input-message') as HTMLDivElement/* HTMLInputElement */; public messageInput: HTMLDivElement/* HTMLInputElement */;
public fileInput = document.getElementById('input-file') as HTMLInputElement; public fileInput = document.getElementById('input-file') as HTMLInputElement;
public inputMessageContainer = document.getElementsByClassName('input-message-container')[0] as HTMLDivElement; public inputMessageContainer = document.getElementsByClassName('input-message-container')[0] as HTMLDivElement;
public inputScroll = new Scrollable(this.inputMessageContainer); public inputScroll = new Scrollable(this.inputMessageContainer);
@ -73,6 +73,15 @@ export class ChatInput {
private helperFunc: () => void; private helperFunc: () => void;
constructor() { constructor() {
const messageInputField = InputField({
placeholder: 'Message',
name: 'message'
});
messageInputField.input.className = '';
this.inputScroll.container.append(messageInputField.input);
this.messageInput = messageInputField.input;
this.attachMenu = document.getElementById('attach-file') as HTMLButtonElement; this.attachMenu = document.getElementById('attach-file') as HTMLButtonElement;
let willAttachType: 'document' | 'media'; let willAttachType: 'document' | 'media';
@ -183,8 +192,6 @@ export class ChatInput {
this.messageInput.addEventListener('input', (e) => { this.messageInput.addEventListener('input', (e) => {
//console.log('messageInput input', this.messageInput.innerText, this.serializeNodes(Array.from(this.messageInput.childNodes))); //console.log('messageInput input', this.messageInput.innerText, this.serializeNodes(Array.from(this.messageInput.childNodes)));
this.setDirection();
const value = this.messageInput.innerText; const value = this.messageInput.innerText;
const entities = RichTextProcessor.parseEntities(value); const entities = RichTextProcessor.parseEntities(value);
@ -217,7 +224,7 @@ export class ChatInput {
} }
} }
if(!value.trim() && !this.serializeNodes(Array.from(this.messageInput.childNodes)).trim()) { if(!value.trim() && !serializeNodes(Array.from(this.messageInput.childNodes)).trim()) {
this.messageInput.innerHTML = ''; this.messageInput.innerHTML = '';
appMessagesManager.setTyping($rootScope.selectedPeerID, 'sendMessageCancelAction'); appMessagesManager.setTyping($rootScope.selectedPeerID, 'sendMessageCancelAction');
@ -232,7 +239,7 @@ export class ChatInput {
this.updateSendBtn(); this.updateSendBtn();
}); });
if(!RichTextProcessor.emojiSupported) { /* if(!RichTextProcessor.emojiSupported) {
this.messageInput.addEventListener('copy', (e) => { this.messageInput.addEventListener('copy', (e) => {
const selection = document.getSelection(); const selection = document.getSelection();
@ -243,7 +250,7 @@ export class ChatInput {
let selectedNodes = Array.from(ancestorContainer.childNodes).slice(range.startOffset, range.endOffset); let selectedNodes = Array.from(ancestorContainer.childNodes).slice(range.startOffset, range.endOffset);
if(selectedNodes.length) { if(selectedNodes.length) {
str = this.serializeNodes(selectedNodes); str = serializeNodes(selectedNodes);
} else { } else {
str = selection.toString(); str = selection.toString();
} }
@ -257,30 +264,7 @@ export class ChatInput {
event.clipboardData.setData('text/plain', str); event.clipboardData.setData('text/plain', str);
event.preventDefault(); event.preventDefault();
}); });
} } */
this.messageInput.addEventListener('paste', (e) => {
//console.log('messageInput paste');
e.preventDefault();
// @ts-ignore
let text = (e.originalEvent || e).clipboardData.getData('text/plain');
let entities = RichTextProcessor.parseEntities(text);
//console.log('messageInput paste', text, entities);
entities = entities.filter(e => e._ == 'messageEntityEmoji' || e._ == 'messageEntityLinebreak');
//text = RichTextProcessor.wrapEmojiText(text);
text = RichTextProcessor.wrapRichText(text, {entities, noLinks: true});
// console.log('messageInput paste after', text);
// @ts-ignore
//let html = (e.originalEvent || e).clipboardData.getData('text/html');
// @ts-ignore
//console.log('paste text', text, );
window.document.execCommand('insertHTML', false, text);
});
this.fileInput.addEventListener('change', (e) => { this.fileInput.addEventListener('change', (e) => {
let files = (e.target as HTMLInputElement & EventTarget).files; let files = (e.target as HTMLInputElement & EventTarget).files;
@ -292,7 +276,7 @@ export class ChatInput {
this.fileInput.value = ''; this.fileInput.value = '';
}, false); }, false);
document.addEventListener('paste', (event) => { document.addEventListener('paste', (e) => {
const peerID = $rootScope.selectedPeerID; const peerID = $rootScope.selectedPeerID;
if(!peerID || $rootScope.overlayIsActive || (peerID < 0 && !appChatsManager.hasRights(peerID, 'send', 'send_media'))) { if(!peerID || $rootScope.overlayIsActive || (peerID < 0 && !appChatsManager.hasRights(peerID, 'send', 'send_media'))) {
return; return;
@ -301,13 +285,15 @@ export class ChatInput {
//console.log('document paste'); //console.log('document paste');
// @ts-ignore // @ts-ignore
var items = (event.clipboardData || event.originalEvent.clipboardData).items; const items = (e.clipboardData || e.originalEvent.clipboardData).items;
//console.log('item', event.clipboardData.getData()); //console.log('item', event.clipboardData.getData());
let foundFile = false;
for(let i = 0; i < items.length; ++i) { for(let i = 0; i < items.length; ++i) {
if(items[i].kind == 'file') { if(items[i].kind == 'file') {
event.preventDefault() e.preventDefault()
event.cancelBubble = true; e.cancelBubble = true;
event.stopPropagation(); e.stopPropagation();
foundFile = true;
let file = items[i].getAsFile(); let file = items[i].getAsFile();
//console.log(items[i], file); //console.log(items[i], file);
@ -541,10 +527,8 @@ export class ChatInput {
}); });
} }
private isInputEmpty() { public isInputEmpty() {
let value = this.messageInput.innerText; return isInputEmpty(this.messageInput);
return !value.trim() && !this.serializeNodes(Array.from(this.messageInput.childNodes)).trim();
} }
public updateSendBtn() { public updateSendBtn() {
@ -557,18 +541,6 @@ export class ChatInput {
this.btnSend.classList.toggle('record', icon == 'record'); this.btnSend.classList.toggle('record', icon == 'record');
} }
public serializeNodes(nodes: Node[]): string {
return nodes.reduce((str, child: any) => {
//console.log('childNode', str, child, typeof(child), typeof(child) === 'string', child.innerText);
if(typeof(child) === 'object' && child.textContent) return str += child.textContent;
if(child.innerText) return str += child.innerText;
if(child.tagName == 'IMG' && child.classList && child.classList.contains('emoji')) return str += child.getAttribute('alt');
return str;
}, '');
};
public onMessageSent(clearInput = true, clearReply?: boolean) { public onMessageSent(clearInput = true, clearReply?: boolean) {
let dialog = appMessagesManager.getDialogByPeerID(appImManager.peerID)[0]; let dialog = appMessagesManager.getDialogByPeerID(appImManager.peerID)[0];
if(dialog && dialog.top_message) { if(dialog && dialog.top_message) {
@ -587,17 +559,6 @@ export class ChatInput {
} }
this.updateSendBtn(); this.updateSendBtn();
this.setDirection();
}
public setDirection() {
const char = this.messageInput.innerText[0];
let direction = 'ltr';
if(char && checkRTL(char)) {
direction = 'rtl';
}
this.messageInput.style.direction = direction;
} }
public sendMessage() { public sendMessage() {
@ -708,7 +669,6 @@ export class ChatInput {
this.editMsgID = 0; this.editMsgID = 0;
this.helperType = this.helperFunc = undefined; this.helperType = this.helperFunc = undefined;
this.chatInput.parentElement.classList.remove('is-helper-active'); this.chatInput.parentElement.classList.remove('is-helper-active');
this.setDirection();
} }
public setTopInfo(type: ChatInputHelperType, callerFunc: () => void, title = '', subtitle = '', input?: string, message?: any) { public setTopInfo(type: ChatInputHelperType, callerFunc: () => void, title = '', subtitle = '', input?: string, message?: any) {
@ -735,7 +695,6 @@ export class ChatInput {
setTimeout(() => { setTimeout(() => {
this.updateSendBtn(); this.updateSendBtn();
this.setDirection();
}, 0); }, 0);
} }

102
src/components/inputField.ts

@ -1,33 +1,119 @@
const InputField = (placeholder: string, label: string, name: string, maxLength?: number, showLengthOn: number = maxLength ? maxLength / 3 : 0) => { import { getRichValue, isInputEmpty } from "../helpers/dom";
import { checkRTL } from "../helpers/string";
import RichTextProcessor from "../lib/richtextprocessor";
let init = () => {
document.addEventListener('paste', (e) => {
if(!(e.target as HTMLElement).hasAttribute('contenteditable') && !(e.target as HTMLElement).parentElement.hasAttribute('contenteditable')) {
return;
}
//console.log('document paste');
//console.log('messageInput paste');
e.preventDefault();
// @ts-ignore
let text = (e.originalEvent || e).clipboardData.getData('text/plain');
let entities = RichTextProcessor.parseEntities(text);
//console.log('messageInput paste', text, entities);
entities = entities.filter(e => e._ == 'messageEntityEmoji' || e._ == 'messageEntityLinebreak');
//text = RichTextProcessor.wrapEmojiText(text);
text = RichTextProcessor.wrapRichText(text, {entities, noLinks: true});
// console.log('messageInput paste after', text);
// @ts-ignore
//let html = (e.originalEvent || e).clipboardData.getData('text/html');
// @ts-ignore
//console.log('paste text', text, );
window.document.execCommand('insertHTML', false, text);
});
init = null;
};
const InputField = (options: {
placeholder?: string,
label?: string,
name: string,
maxLength?: number,
showLengthOn?: number,
plainText?: true
}) => {
const div = document.createElement('div'); const div = document.createElement('div');
div.classList.add('input-field'); div.classList.add('input-field');
if(options.maxLength) {
options.showLengthOn = Math.round(options.maxLength / 3);
}
const {placeholder, label, maxLength, showLengthOn, name, plainText} = options;
if(!plainText) {
if(init) {
init();
}
div.innerHTML = `
<div id="input-${name}" ${placeholder ? `data-placeholder="${placeholder}"` : ''} contenteditable="true" class="input-field-input"></div>
${label ? `<label for="input-${name}">${label}</label>` : ''}
`;
const input = div.firstElementChild as HTMLElement;
const observer = new MutationObserver((mutationsList, observer) => {
const isEmpty = isInputEmpty(input);
console.log('input', isEmpty);
const char = input.innerText[0];
let direction = 'ltr';
if(char && checkRTL(char)) {
direction = 'rtl';
}
input.style.direction = direction;
if(processInput) {
processInput();
}
});
// ! childList for paste first symbol
observer.observe(input, {characterData: true, childList: true, subtree: true});
} else {
div.innerHTML = ` div.innerHTML = `
<input type="text" name="${name}" id="input-${name}" placeholder="${placeholder}" autocomplete="off" required=""> <input type="text" name="${name}" id="input-${name}" ${placeholder ? `placeholder="${placeholder}"` : ''} autocomplete="off" required="" class="input-field-input">
<label for="input-${name}">${label}</label> ${label ? `<label for="input-${name}">${label}</label>` : ''}
`; `;
}
let processInput: () => void;
if(maxLength) { if(maxLength) {
const input = div.firstElementChild as HTMLInputElement; const input = div.firstElementChild as HTMLInputElement;
const labelEl = div.lastElementChild as HTMLLabelElement; const labelEl = div.lastElementChild as HTMLLabelElement;
let showingLength = false; let showingLength = false;
input.addEventListener('input', (e) => {
processInput = () => {
const wasError = input.classList.contains('error'); const wasError = input.classList.contains('error');
const diff = maxLength - input.value.length; const inputLength = plainText ? input.value.length : getRichValue(input).length;
const diff = maxLength - inputLength;
const isError = diff < 0; const isError = diff < 0;
input.classList.toggle('error', isError); input.classList.toggle('error', isError);
if(isError || diff <= showLengthOn) { if(isError || diff <= showLengthOn) {
labelEl.innerText = label + ` (${maxLength - input.value.length})`; labelEl.innerText = label + ` (${maxLength - inputLength})`;
if(!showingLength) showingLength = true; if(!showingLength) showingLength = true;
} else if((wasError && !isError) || showingLength) { } else if((wasError && !isError) || showingLength) {
labelEl.innerText = label; labelEl.innerText = label;
showingLength = false; showingLength = false;
} }
}); };
input.addEventListener('input', processInput);
} }
return div; return {container: div, input: div.firstElementChild as HTMLInputElement};
}; };
export default InputField; export default InputField;

51
src/components/popupCreatePoll.ts

@ -2,7 +2,7 @@ import appMessagesManager from "../lib/appManagers/appMessagesManager";
import appPeersManager from "../lib/appManagers/appPeersManager"; import appPeersManager from "../lib/appManagers/appPeersManager";
import appPollsManager, { Poll } from "../lib/appManagers/appPollsManager"; import appPollsManager, { Poll } from "../lib/appManagers/appPollsManager";
import $rootScope from "../lib/rootScope"; import $rootScope from "../lib/rootScope";
import { findUpTag, whichChild } from "../helpers/dom"; import { cancelEvent, findUpTag, getRichValue, isInputEmpty, whichChild } from "../helpers/dom";
import CheckboxField from "./checkbox"; import CheckboxField from "./checkbox";
import InputField from "./inputField"; import InputField from "./inputField";
import { PopupElement } from "./popup"; import { PopupElement } from "./popup";
@ -32,10 +32,15 @@ export default class PopupCreatePoll extends PopupElement {
this.title.innerText = 'New Poll'; this.title.innerText = 'New Poll';
const questionField = InputField('Ask a Question', 'Ask a Question', 'question', MAX_LENGTH_QUESTION); const questionField = InputField({
this.questionInput = questionField.firstElementChild as HTMLInputElement; placeholder: 'Ask a Question',
label: 'Ask a Question',
name: 'question',
maxLength: MAX_LENGTH_QUESTION
});
this.questionInput = questionField.input;
this.header.append(questionField); this.header.append(questionField.container);
const hr = document.createElement('hr'); const hr = document.createElement('hr');
const d = document.createElement('div'); const d = document.createElement('div');
@ -93,14 +98,19 @@ export default class PopupCreatePoll extends PopupElement {
const quizSolutionContainer = document.createElement('div'); const quizSolutionContainer = document.createElement('div');
quizSolutionContainer.classList.add('poll-create-questions'); quizSolutionContainer.classList.add('poll-create-questions');
const quizSolutionField = InputField('Add a Comment (Optional)', 'Add a Comment (Optional)', 'solution', MAX_LENGTH_SOLUTION); const quizSolutionField = InputField({
this.quizSolutionInput = quizSolutionField.firstElementChild as HTMLInputElement; placeholder: 'Add a Comment (Optional)',
label: 'Add a Comment (Optional)',
name: 'solution',
maxLength: MAX_LENGTH_SOLUTION
});
this.quizSolutionInput = quizSolutionField.input;
const quizSolutionSubtitle = document.createElement('div'); const quizSolutionSubtitle = document.createElement('div');
quizSolutionSubtitle.classList.add('subtitle'); quizSolutionSubtitle.classList.add('subtitle');
quizSolutionSubtitle.innerText = 'Users will see this comment after choosing a wrong answer, good for educational purposes.'; quizSolutionSubtitle.innerText = 'Users will see this comment after choosing a wrong answer, good for educational purposes.';
quizSolutionContainer.append(quizSolutionField, quizSolutionSubtitle); quizSolutionContainer.append(quizSolutionField.container, quizSolutionSubtitle);
quizElements.push(quizHr, quizSolutionCaption, quizSolutionContainer); quizElements.push(quizHr, quizSolutionCaption, quizSolutionContainer);
quizElements.forEach(el => el.classList.add('hide')); quizElements.forEach(el => el.classList.add('hide'));
@ -120,15 +130,15 @@ export default class PopupCreatePoll extends PopupElement {
private getFilledAnswers() { private getFilledAnswers() {
const answers = Array.from(this.questions.children).map((el, idx) => { const answers = Array.from(this.questions.children).map((el, idx) => {
const input = el.querySelector('input[type="text"]') as HTMLInputElement; const input = el.querySelector('.input-field-input');
return input.value; return getRichValue(input);
}).filter(v => !!v.trim()); }).filter(v => !!v.trim());
return answers; return answers;
} }
onSubmitClick = (e: MouseEvent) => { onSubmitClick = (e: MouseEvent) => {
const question = this.questionInput.value.trim(); const question = getRichValue(this.questionInput);
if(!question) { if(!question) {
toast('Please enter a question.'); toast('Please enter a question.');
@ -158,7 +168,7 @@ export default class PopupCreatePoll extends PopupElement {
return; return;
} }
const quizSolution = this.quizSolutionInput.value.trim() || undefined; const quizSolution = getRichValue(this.quizSolutionInput) || undefined;
if(quizSolution?.length > MAX_LENGTH_SOLUTION) { if(quizSolution?.length > MAX_LENGTH_SOLUTION) {
toast('Explanation is too long.'); toast('Explanation is too long.');
return; return;
@ -210,14 +220,15 @@ export default class PopupCreatePoll extends PopupElement {
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
const radioLabel = findUpTag(target, 'LABEL'); const radioLabel = findUpTag(target, 'LABEL');
if(target.value.length) { const isEmpty = isInputEmpty(target);
if(!isEmpty) {
target.parentElement.classList.add('is-filled'); target.parentElement.classList.add('is-filled');
radioLabel.classList.remove('hidden-widget'); radioLabel.classList.remove('hidden-widget');
radioLabel.firstElementChild.removeAttribute('disabled'); radioLabel.firstElementChild.removeAttribute('disabled');
} }
const isLast = !radioLabel.nextElementSibling; const isLast = !radioLabel.nextElementSibling;
if(isLast && target.value.length && this.questions.childElementCount < 10) { if(isLast && !isEmpty && this.questions.childElementCount < 10) {
this.appendMoreField(); this.appendMoreField();
} }
}; };
@ -235,11 +246,17 @@ export default class PopupCreatePoll extends PopupElement {
private appendMoreField() { private appendMoreField() {
const tempID = this.tempID++; const tempID = this.tempID++;
const idx = this.questions.childElementCount + 1; const idx = this.questions.childElementCount + 1;
const questionField = InputField('Add an Option', 'Option ' + idx, 'question-' + tempID, MAX_LENGTH_OPTION); const questionField = InputField({
(questionField.firstElementChild as HTMLInputElement).addEventListener('input', this.onInput); placeholder: 'Add an Option',
label: 'Option ' + idx,
name: 'question-' + tempID,
maxLength: MAX_LENGTH_OPTION
});
questionField.input.addEventListener('input', this.onInput);
const radioField = RadioField('', 'question'); const radioField = RadioField('', 'question');
radioField.main.append(questionField); radioField.main.append(questionField.container);
radioField.main.addEventListener('click', cancelEvent);
radioField.label.classList.add('hidden-widget'); radioField.label.classList.add('hidden-widget');
radioField.input.disabled = true; radioField.input.disabled = true;
if(!this.quizCheckboxField.input.checked) { if(!this.quizCheckboxField.input.checked) {
@ -255,7 +272,7 @@ export default class PopupCreatePoll extends PopupElement {
const deleteBtn = document.createElement('span'); const deleteBtn = document.createElement('span');
deleteBtn.classList.add('btn-icon', 'tgico-close'); deleteBtn.classList.add('btn-icon', 'tgico-close');
questionField.append(deleteBtn); questionField.container.append(deleteBtn);
deleteBtn.addEventListener('click', this.onDeleteClick, {once: true}); deleteBtn.addEventListener('click', this.onDeleteClick, {once: true});

16
src/components/popupNewMedia.ts

@ -1,7 +1,7 @@
import { isTouchSupported } from "../helpers/touchSupport"; import { isTouchSupported } from "../helpers/touchSupport";
import appImManager from "../lib/appManagers/appImManager"; import appImManager from "../lib/appManagers/appImManager";
import appMessagesManager from "../lib/appManagers/appMessagesManager"; import appMessagesManager from "../lib/appManagers/appMessagesManager";
import { calcImageInBox } from "../helpers/dom"; import { calcImageInBox, getRichValue } from "../helpers/dom";
import { Layouter, RectPart } from "./groupedLayout"; import { Layouter, RectPart } from "./groupedLayout";
import InputField from "./inputField"; import InputField from "./inputField";
import { PopupElement } from "./popup"; import { PopupElement } from "./popup";
@ -53,9 +53,15 @@ export default class PopupNewMedia extends PopupElement {
const scrollable = new Scrollable(null); const scrollable = new Scrollable(null);
scrollable.container.append(this.mediaContainer); scrollable.container.append(this.mediaContainer);
const inputField = InputField('Add a caption...', 'Caption', 'photo-caption', MAX_LENGTH_CAPTION, 80); const inputField = InputField({
this.input = inputField.firstElementChild as HTMLInputElement; placeholder: 'Add a caption...',
this.container.append(scrollable.container, inputField); label: 'Caption',
name: 'photo-caption',
maxLength: MAX_LENGTH_CAPTION,
showLengthOn: 80
});
this.input = inputField.input;
this.container.append(scrollable.container, inputField.container);
this.attachFiles(files); this.attachFiles(files);
} }
@ -72,7 +78,7 @@ export default class PopupNewMedia extends PopupElement {
}; };
public send = () => { public send = () => {
let caption = this.input.value.trim(); let caption = getRichValue(this.input);
if(caption.length > MAX_LENGTH_CAPTION) { if(caption.length > MAX_LENGTH_CAPTION) {
toast('Caption is too long.'); toast('Caption is too long.');
return; return;

4
src/components/scrollable.ts

@ -158,7 +158,7 @@ export default class Scrollable extends ScrollableBase {
}); });
}; };
public checkForTriggers() { public checkForTriggers = () => {
if(this.scrollLocked || (!this.onScrolledTop && !this.onScrolledBottom)) return; if(this.scrollLocked || (!this.onScrolledTop && !this.onScrolledBottom)) return;
const container = this.container; const container = this.container;
@ -179,7 +179,7 @@ export default class Scrollable extends ScrollableBase {
if(this.onScrolledBottom && (maxScrollTop - scrollTop) <= this.onScrollOffset) { if(this.onScrolledBottom && (maxScrollTop - scrollTop) <= this.onScrollOffset) {
this.onScrolledBottom(); this.onScrolledBottom();
} }
} };
public prepend(element: HTMLElement) { public prepend(element: HTMLElement) {
(this.splitUp || this.container).prepend(element); (this.splitUp || this.container).prepend(element);

2
src/components/sidebarLeft/tabs/archivedTab.ts

@ -19,7 +19,7 @@ export default class AppArchivedTab implements SliderTab {
appDialogsManager.setListClickListener(this.chatList, null, true); appDialogsManager.setListClickListener(this.chatList, null, true);
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
setTimeout(appDialogsManager.onChatsScroll, 0); setTimeout(appDialogsManager.scroll.checkForTriggers, 0);
}); });
} }

141
src/components/sidebarLeft/tabs/editProfile.ts

@ -1,9 +1,13 @@
import appSidebarLeft from ".."; import appSidebarLeft from "..";
import { getRichValue } from "../../../helpers/dom";
import { InputFile } from "../../../layer"; import { InputFile } from "../../../layer";
import appProfileManager from "../../../lib/appManagers/appProfileManager"; import appProfileManager from "../../../lib/appManagers/appProfileManager";
import appUsersManager from "../../../lib/appManagers/appUsersManager"; import appUsersManager from "../../../lib/appManagers/appUsersManager";
import apiManager from "../../../lib/mtproto/mtprotoworker"; import apiManager from "../../../lib/mtproto/mtprotoworker";
import RichTextProcessor from "../../../lib/richtextprocessor";
import $rootScope from "../../../lib/rootScope"; import $rootScope from "../../../lib/rootScope";
import AvatarElement from "../../avatar";
import InputField from "../../inputField";
import PopupAvatar from "../../popupAvatar"; import PopupAvatar from "../../popupAvatar";
import Scrollable from "../../scrollable"; import Scrollable from "../../scrollable";
import { SliderTab } from "../../slider"; import { SliderTab } from "../../slider";
@ -11,21 +15,21 @@ import { SliderTab } from "../../slider";
// TODO: аватарка не поменяется в этой вкладке после изменения почему-то (если поставить в другом клиенте, и потом тут проверить, для этого ещё вышел в чатлист) // TODO: аватарка не поменяется в этой вкладке после изменения почему-то (если поставить в другом клиенте, и потом тут проверить, для этого ещё вышел в чатлист)
export default class AppEditProfileTab implements SliderTab { export default class AppEditProfileTab implements SliderTab {
private container = document.querySelector('.edit-profile-container') as HTMLDivElement; private container: HTMLElement;
private scrollWrapper = this.container.querySelector('.scroll-wrapper') as HTMLDivElement; private scrollWrapper: HTMLElement;
private nextBtn = this.container.querySelector('.btn-corner') as HTMLButtonElement; private nextBtn: HTMLButtonElement;
private canvas = this.container.querySelector('.avatar-edit-canvas') as HTMLCanvasElement; private canvas: HTMLCanvasElement;
private uploadAvatar: () => Promise<InputFile> = null; private uploadAvatar: () => Promise<InputFile> = null;
private firstNameInput = this.container.querySelector('.firstname') as HTMLInputElement; private firstNameInput: HTMLInputElement;
private lastNameInput = this.container.querySelector('.lastname') as HTMLInputElement; private lastNameInput: HTMLInputElement;
private bioInput = this.container.querySelector('.bio') as HTMLInputElement; private bioInput: HTMLInputElement;
private userNameInput = this.container.querySelector('.username') as HTMLInputElement; private userNameInput: HTMLInputElement;
private avatarElem = document.createElement('avatar-element'); private avatarElem: AvatarElement;
private profileUrlContainer = this.container.querySelector('.profile-url-container') as HTMLDivElement; private profileUrlContainer: HTMLDivElement;
private profileUrlAnchor = this.profileUrlContainer.lastElementChild as HTMLAnchorElement; private profileUrlAnchor: HTMLAnchorElement;
private originalValues = { private originalValues = {
firstName: '', firstName: '',
@ -34,8 +38,20 @@ export default class AppEditProfileTab implements SliderTab {
bio: '' bio: ''
}; };
constructor() { public init() {
this.container.querySelector('.avatar-edit').addEventListener('click', () => { this.container = document.querySelector('.edit-profile-container');
this.scrollWrapper = this.container.querySelector('.scroll-wrapper');
this.nextBtn = this.container.querySelector('.btn-corner');
this.canvas = this.container.querySelector('.avatar-edit-canvas');
this.avatarElem = document.createElement('avatar-element') as AvatarElement;
this.avatarElem.classList.add('avatar-placeholder');
this.profileUrlContainer = this.container.querySelector('.profile-url-container');
this.profileUrlAnchor = this.profileUrlContainer.lastElementChild as HTMLAnchorElement;
const avatarEdit = this.container.querySelector('.avatar-edit');
avatarEdit.addEventListener('click', () => {
new PopupAvatar().open(this.canvas, (_upload) => { new PopupAvatar().open(this.canvas, (_upload) => {
this.uploadAvatar = _upload; this.uploadAvatar = _upload;
this.handleChange(); this.handleChange();
@ -43,13 +59,56 @@ export default class AppEditProfileTab implements SliderTab {
}); });
}); });
this.avatarElem.classList.add('avatar-placeholder'); {
const inputWrapper = document.createElement('div');
inputWrapper.classList.add('input-wrapper');
const firstNameInputField = InputField({
label: 'Name',
name: 'first-name',
maxLength: 70
});
const lastNameInputField = InputField({
label: 'Last Name',
name: 'last-name',
maxLength: 64
});
const bioInputField = InputField({
label: 'Bio (optional)',
name: 'bio',
maxLength: 70
});
this.firstNameInput = firstNameInputField.input;
this.lastNameInput = lastNameInputField.input;
this.bioInput = bioInputField.input;
inputWrapper.append(firstNameInputField.container, lastNameInputField.container, bioInputField.container);
avatarEdit.parentElement.insertBefore(inputWrapper, avatarEdit.nextElementSibling);
}
{
const inputWrapper = document.createElement('div');
inputWrapper.classList.add('input-wrapper');
const userNameInputField = InputField({
label: 'Username (optional)',
name: 'username',
plainText: true
});
this.userNameInput = userNameInputField.input;
inputWrapper.append(userNameInputField.container);
const caption = this.profileUrlContainer.parentElement;
caption.parentElement.insertBefore(inputWrapper, caption);
}
let userNameLabel = this.userNameInput.nextElementSibling as HTMLLabelElement; let userNameLabel = this.userNameInput.nextElementSibling as HTMLLabelElement;
this.firstNameInput.addEventListener('input', () => this.handleChange()); this.firstNameInput.addEventListener('input', this.handleChange);
this.lastNameInput.addEventListener('input', () => this.handleChange()); this.lastNameInput.addEventListener('input', this.handleChange);
this.bioInput.addEventListener('input', () => this.handleChange()); this.bioInput.addEventListener('input', this.handleChange);
this.userNameInput.addEventListener('input', () => { this.userNameInput.addEventListener('input', () => {
let value = this.userNameInput.value; let value = this.userNameInput.value;
@ -110,8 +169,7 @@ export default class AppEditProfileTab implements SliderTab {
let promises: Promise<any>[] = []; let promises: Promise<any>[] = [];
promises.push(appProfileManager.updateProfile(getRichValue(this.firstNameInput), getRichValue(this.lastNameInput), getRichValue(this.bioInput)).then(() => {
promises.push(appProfileManager.updateProfile(this.firstNameInput.value, this.lastNameInput.value, this.bioInput.value).then(() => {
appSidebarLeft.selectTab(0); appSidebarLeft.selectTab(0);
}, (err) => { }, (err) => {
console.error('updateProfile error:', err); console.error('updateProfile error:', err);
@ -127,10 +185,8 @@ export default class AppEditProfileTab implements SliderTab {
promises.push(appProfileManager.updateUsername(this.userNameInput.value)); promises.push(appProfileManager.updateUsername(this.userNameInput.value));
} }
Promise.race(promises).then(() => { Promise.race(promises).finally(() => {
this.nextBtn.disabled = false; this.nextBtn.removeAttribute('disabled');
}, () => {
this.nextBtn.disabled = false;
}); });
}); });
@ -138,17 +194,34 @@ export default class AppEditProfileTab implements SliderTab {
} }
public fillElements() { public fillElements() {
let user = appUsersManager.getSelf(); if(this.init) {
this.firstNameInput.value = this.originalValues.firstName = user.first_name ?? ''; this.init();
this.lastNameInput.value = this.originalValues.lastName = user.last_name ?? ''; this.init = null;
}
const user = appUsersManager.getSelf();
Object.assign(this.originalValues, {
firstName: user.first_name,
lastName: user.last_name,
userName: user.username
});
this.firstNameInput.innerHTML = user.rFirstName;
this.lastNameInput.innerHTML = RichTextProcessor.wrapRichText(user.last_name, {noLinks: true, noLinebreaks: true});
this.userNameInput.value = this.originalValues.userName = user.username ?? ''; this.userNameInput.value = this.originalValues.userName = user.username ?? '';
this.firstNameInput.classList.remove('error');
this.lastNameInput.classList.remove('error');
this.bioInput.classList.remove('error');
this.userNameInput.classList.remove('valid', 'error'); this.userNameInput.classList.remove('valid', 'error');
this.userNameInput.nextElementSibling.innerHTML = 'Username (optional)'; this.userNameInput.nextElementSibling.innerHTML = 'Username (optional)';
appProfileManager.getProfile(user.id).then(userFull => { appProfileManager.getProfile(user.id, true).then(userFull => {
if(userFull.rAbout) { if(userFull.rAbout) {
this.bioInput.value = this.originalValues.bio = userFull.rAbout; this.originalValues.bio = userFull.about;
this.bioInput.innerHTML = userFull.rAbout;
} }
}); });
@ -168,10 +241,10 @@ export default class AppEditProfileTab implements SliderTab {
private isChanged() { private isChanged() {
return !!this.uploadAvatar return !!this.uploadAvatar
|| this.firstNameInput.value != this.originalValues.firstName || (!this.firstNameInput.classList.contains('error') && getRichValue(this.firstNameInput) != this.originalValues.firstName)
|| this.lastNameInput.value != this.originalValues.lastName || (!this.lastNameInput.classList.contains('error') && getRichValue(this.lastNameInput) != this.originalValues.lastName)
|| (this.userNameInput.value != this.originalValues.userName && !this.userNameInput.classList.contains('error')) || (!this.bioInput.classList.contains('error') && getRichValue(this.bioInput) != this.originalValues.bio)
|| this.bioInput.value != this.originalValues.bio; || (this.userNameInput.value != this.originalValues.userName && !this.userNameInput.classList.contains('error'));
} }
private setProfileUrl() { private setProfileUrl() {
@ -185,13 +258,13 @@ export default class AppEditProfileTab implements SliderTab {
} }
} }
private handleChange() { private handleChange = () => {
if(this.isChanged()) { if(this.isChanged()) {
this.nextBtn.classList.add('is-visible'); this.nextBtn.classList.add('is-visible');
} else { } else {
this.nextBtn.classList.remove('is-visible'); this.nextBtn.classList.remove('is-visible');
} }
} };
onCloseAfterTimeout() { onCloseAfterTimeout() {
this.nextBtn.classList.remove('is-visible'); this.nextBtn.classList.remove('is-visible');

22
src/helpers/dom.ts

@ -127,6 +127,28 @@ export function getRichElementValue(node: any, lines: string[], line: string[],
} }
} }
export function isInputEmpty(element: HTMLElement) {
if(element.hasAttribute('contenteditable') || element.tagName != 'INPUT') {
const value = element.innerText;
return !value.trim() && !serializeNodes(Array.from(element.childNodes)).trim();
} else {
return !(element as HTMLInputElement).value.trim().length;
}
}
export function serializeNodes(nodes: Node[]): string {
return nodes.reduce((str, child: any) => {
//console.log('childNode', str, child, typeof(child), typeof(child) === 'string', child.innerText);
if(typeof(child) === 'object' && child.textContent) return str += child.textContent;
if(child.innerText) return str += child.innerText;
if(child.tagName == 'IMG' && child.classList && child.classList.contains('emoji')) return str += child.getAttribute('alt');
return str;
}, '');
}
/* if (Config.Modes.animations && /* if (Config.Modes.animations &&
typeof window.requestAnimationFrame == 'function') { typeof window.requestAnimationFrame == 'function') {
window.onAnimationFrameCallback = function (cb) { window.onAnimationFrameCallback = function (cb) {

34
src/index.hbs

@ -168,7 +168,7 @@
</div> </div>
</div> </div>
<div class="transition-item sidebar-search" id="search-container"></div> <div class="transition-item sidebar-search" id="search-container"></div>
<button class="btn-primary btn-circle btn-icon rp btn-corner tgico-newchat_filled btn-menu-toggle" id="new-menu"> <button class="btn-circle rp btn-corner tgico-newchat_filled btn-menu-toggle" id="new-menu">
<div class="btn-menu top-left"> <div class="btn-menu top-left">
<div class="btn-menu-item menu-channel tgico-newchannel rp">New Channel</div> <div class="btn-menu-item menu-channel tgico-newchannel rp">New Channel</div>
<div class="btn-menu-item menu-group tgico-newgroup rp">New Group</div> <div class="btn-menu-item menu-group tgico-newgroup rp">New Group</div>
@ -219,7 +219,7 @@
</div> </div>
</div> </div>
<div class="caption">You can provide an optional description for your channel.</div> <div class="caption">You can provide an optional description for your channel.</div>
<button class="btn-primary btn-circle btn-icon rp btn-corner tgico-next"></button> <button class="btn-circle rp btn-corner tgico-next"></button>
</div> </div>
</div> </div>
<div class="sidebar-slider-item addmembers-container"> <div class="sidebar-slider-item addmembers-container">
@ -228,7 +228,7 @@
<div class="sidebar-header__title">Add Members</div> <div class="sidebar-header__title">Add Members</div>
</div> </div>
<div class="sidebar-content"> <div class="sidebar-content">
<button class="btn-primary btn-circle btn-icon rp btn-corner tgico-next"></button> <button class="btn-circle rp btn-corner tgico-next"></button>
</div> </div>
</div> </div>
<div class="sidebar-slider-item new-group-container"> <div class="sidebar-slider-item new-group-container">
@ -247,7 +247,7 @@
<label for="new-group-name">Group Name</label> <label for="new-group-name">Group Name</label>
</div> </div>
</div> </div>
<button class="btn-primary btn-circle btn-icon rp btn-corner tgico-next"></button> <button class="btn-circle rp btn-corner tgico-next"></button>
</div> </div>
</div> </div>
<div class="sidebar-slider-item settings-container"> <div class="sidebar-slider-item settings-container">
@ -287,34 +287,14 @@
<canvas class="avatar-edit-canvas"></canvas> <canvas class="avatar-edit-canvas"></canvas>
<span class="tgico tgico-cameraadd"></span> <span class="tgico tgico-cameraadd"></span>
</div> </div>
<div class="input-wrapper">
<div class="input-field">
<input type="text" name="name" class="firstname" autocomplete="xxDDqqOXJXC" id="settings-name" required="">
<label for="settings-name">Name</label>
</div>
<div class="input-field">
<input type="text" name="lastname" class="lastname" autocomplete="aintsofunnowzXCFF" id="settings-lastname" required="">
<label for="settings-lastname">Last Name</label>
</div>
<div class="input-field">
<input type="text" name="bio" class="bio" autocomplete="aintsofunnowhHQ" id="settings-bio" required="">
<label for="settings-bio">Bio (optional)</label>
</div>
</div>
<div class="caption">Any details such as age, occupation or city. Example:<br>23 y.o. designer from San Francisco.</div> <div class="caption">Any details such as age, occupation or city. Example:<br>23 y.o. designer from San Francisco.</div>
<hr/> <hr/>
<div class="sidebar-left-h2">Username</div> <div class="sidebar-left-h2">Username</div>
<div class="input-wrapper">
<div class="input-field">
<input type="text" name="username" class="username" autocomplete="xxDDqqOXffEER" id="settings-username" required="">
<label for="settings-username">Username (optional)</label>
</div>
</div>
<div class="caption">You can choose a username on Telegram. If you do, other people will be able to find you by this username and contact you without knowing your phone number.<br><br>You can use a-z, 0-9 and underscores. Minimum length is 5 characters.<br><br><div class="profile-url-container">This link opens a chat with you: <div class="caption">You can choose a username on Telegram. If you do, other people will be able to find you by this username and contact you without knowing your phone number.<br><br>You can use a-z, 0-9 and underscores. Minimum length is 5 characters.<br><br><div class="profile-url-container">This link opens a chat with you:
<br><a class="profile-url" href="#" target="_blank"></a></div> <br><a class="profile-url" href="#" target="_blank"></a></div>
</div> </div>
</div> </div>
<button class="btn-primary btn-circle btn-icon rp btn-corner tgico-check"></button> <button class="btn-circle rp btn-corner tgico-check"></button>
</div> </div>
</div> </div>
<div class="sidebar-slider-item chat-folders-container"> <div class="sidebar-slider-item chat-folders-container">
@ -447,9 +427,7 @@
<div class="new-message-wrapper"> <div class="new-message-wrapper">
<button class="btn-icon rp tgico toggle-emoticons" id="toggle-emoticons"></button> <button class="btn-icon rp tgico toggle-emoticons" id="toggle-emoticons"></button>
<!-- <textarea type="text" id="input-message" placeholder="Message" contenteditable="true"></textarea> --> <!-- <textarea type="text" id="input-message" placeholder="Message" contenteditable="true"></textarea> -->
<div class="input-message-container"> <div class="input-message-container"></div>
<div id="input-message" contenteditable="true" data-placeholder="Message"></div>
</div>
<button class="btn-icon tgico-attach btn-menu-toggle" id="attach-file"></button> <button class="btn-icon tgico-attach btn-menu-toggle" id="attach-file"></button>
<div class="record-time"></div> <div class="record-time"></div>
<input type="file" id="input-file" style="display: none;" multiple /> <input type="file" id="input-file" style="display: none;" multiple />

14
src/lib/appManagers/appChatsManager.ts

@ -68,7 +68,7 @@ export class AppChatsManager {
public chats: {[id: number]: Channel | Chat | any} = {}; public chats: {[id: number]: Channel | Chat | any} = {};
//public usernames: any = {}; //public usernames: any = {};
//public channelAccess: any = {}; //public channelAccess: any = {};
public megagroups: {[id: number]: true} = {}; //public megagroups: {[id: number]: true} = {};
public cachedPhotoLocations: {[id: number]: any} = {}; public cachedPhotoLocations: {[id: number]: any} = {};
public megagroupOnlines: {[id: number]: {timestamp: number, onlines: number}} = {}; public megagroupOnlines: {[id: number]: {timestamp: number, onlines: number}} = {};
@ -253,13 +253,13 @@ export class AppChatsManager {
this.channelAccess[id] = accessHash; this.channelAccess[id] = accessHash;
} */ } */
public saveIsMegagroup(id: number) { /* public saveIsMegagroup(id: number) {
this.megagroups[id] = true; this.megagroups[id] = true;
} } */
public isChannel(id: number) { public isChannel(id: number) {
if(id < 0) id = -id; if(id < 0) id = -id;
let chat = this.chats[id]; const chat = this.chats[id];
if(chat && (chat._ == 'channel' || chat._ == 'channelForbidden')/* || this.channelAccess[id] */) { if(chat && (chat._ == 'channel' || chat._ == 'channelForbidden')/* || this.channelAccess[id] */) {
return true; return true;
} }
@ -267,11 +267,11 @@ export class AppChatsManager {
} }
public isMegagroup(id: number) { public isMegagroup(id: number) {
if(this.megagroups[id]) { /* if(this.megagroups[id]) {
return true; return true;
} } */
let chat = this.chats[id]; const chat = this.chats[id];
if(chat && chat._ == 'channel' && chat.pFlags.megagroup) { if(chat && chat._ == 'channel' && chat.pFlags.megagroup) {
return true; return true;
} }

2
src/lib/appManagers/appUsersManager.ts

@ -242,7 +242,7 @@ export class AppUsersManager {
const fullName = user.first_name + ' ' + (user.last_name || ''); const fullName = user.first_name + ' ' + (user.last_name || '');
if(user.first_name) { if(user.first_name) {
user.rFirstName = RichTextProcessor.wrapRichText(user.first_name, {noLinks: true, noLinebreaks: true}) user.rFirstName = RichTextProcessor.wrapRichText(user.first_name, {noLinks: true, noLinebreaks: true});
user.rFullName = user.last_name ? RichTextProcessor.wrapRichText(fullName, {noLinks: true, noLinebreaks: true}) : user.rFirstName; user.rFullName = user.last_name ? RichTextProcessor.wrapRichText(fullName, {noLinks: true, noLinebreaks: true}) : user.rFirstName;
} else { } else {
user.rFirstName = RichTextProcessor.wrapRichText(user.last_name, {noLinks: true, noLinebreaks: true}) || user.rPhone || 'user_first_name_deleted'; user.rFirstName = RichTextProcessor.wrapRichText(user.last_name, {noLinks: true, noLinebreaks: true}) || user.rPhone || 'user_first_name_deleted';

6
src/lib/richtextprocessor.ts

@ -809,15 +809,15 @@ namespace RichTextProcessor {
let first = '', last = ''; let first = '', last = '';
const firstNode = childNodes[0]; const firstNode = childNodes[0];
if('length' in firstNode) first = (firstNode as any).textContent.charAt(0).toUpperCase(); if('length' in firstNode) first = (firstNode as any).textContent.trim().charAt(0).toUpperCase();
else first = (firstNode as HTMLElement).outerHTML; else first = (firstNode as HTMLElement).outerHTML;
if(onlyFirst) return first; if(onlyFirst) return first;
if(str.indexOf(' ') !== -1) { if(str.indexOf(' ') !== -1) {
const lastNode = childNodes[childNodes.length - 1]; const lastNode = childNodes[childNodes.length - 1];
if(lastNode == firstNode) last = lastNode.textContent.split(' ').pop().charAt(0).toUpperCase(); if(lastNode == firstNode) last = lastNode.textContent.split(' ').pop().trim().charAt(0).toUpperCase();
else if('length' in lastNode) last = (lastNode as any).textContent.charAt(0).toUpperCase(); else if('length' in lastNode) last = (lastNode as any).textContent.trim().charAt(0).toUpperCase();
else last = (lastNode as HTMLElement).outerHTML; else last = (lastNode as HTMLElement).outerHTML;
} }

19
src/scss/partials/_button.scss

@ -42,6 +42,18 @@
transform: translate3d(0, var(--translateY), 0); transform: translate3d(0, var(--translateY), 0);
z-index: 3; z-index: 3;
user-select: none; user-select: none;
background-color: $color-blue;
text-align: center;
font-size: 1.5rem;
color: #fff;
border: none;
outline: none;
cursor: pointer;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
&.is-visible { &.is-visible {
--translateY: 0; --translateY: 0;
@ -50,6 +62,11 @@
body.animation-level-0 & { body.animation-level-0 & {
transition: none !important; transition: none !important;
} }
&:disabled {
opacity: 1 !important;
pointer-events: all !important;
}
} }
.btn-menu { .btn-menu {
@ -238,7 +255,7 @@
cursor: pointer !important; cursor: pointer !important;
pointer-events: all !important; pointer-events: all !important;
&:not(.btn-primary).menu-open { &:not(.btn-primary):not(.btn-corner).menu-open {
background-color: var(--color-gray-hover); background-color: var(--color-gray-hover);
} }

9
src/scss/partials/_chat.scss

@ -290,7 +290,6 @@ $chat-helper-size: 39px;
resize: none; resize: none;
border: none; border: none;
outline: none; outline: none;
cursor: text;
@media only screen and (max-height: 30rem) { @media only screen and (max-height: 30rem) {
max-height: unquote('max(39px, calc(100vh - 10rem))'); max-height: unquote('max(39px, calc(100vh - 10rem))');
@ -303,13 +302,6 @@ $chat-helper-size: 39px;
/* span.emoji { /* span.emoji {
font-size: .95rem; font-size: .95rem;
} */ } */
//[contenteditable=true]:empty:before {
&:empty:before {
content: attr(data-placeholder);
color: #a2acb4;
display: block; /* For Firefox By Ariel Flesler */
}
} }
.toggle-emoticons { .toggle-emoticons {
@ -967,7 +959,6 @@ $chat-helper-size: 39px;
box-shadow: 0 1px 2px 0 rgba(16, 35, 47, .07); box-shadow: 0 1px 2px 0 rgba(16, 35, 47, .07);
min-height: $chat-input-size; min-height: $chat-input-size;
max-height: 30rem; max-height: 30rem;
caret-color: $button-primary-background;
flex: 0 0 auto; flex: 0 0 auto;
position: relative; position: relative;
z-index: 3; z-index: 3;

155
src/scss/partials/_input.scss

@ -0,0 +1,155 @@
.input-wrapper {
width: 360px;
margin: 0 auto;
}
.input-field {
position: relative;
.arrow-down {
position: absolute;
content: " ";
top: 50%;
bottom: 0;
right: 21px;
cursor: pointer;
height: 0;
width: 0;
border: solid #707579;
border-radius: 1px;
border-width: 0 2px 2px 0;
display: inline-block;
padding: 5px;
vertical-align: middle;
z-index: 2;
margin-top: -9px;
transform: rotate(45deg);
-webkit-transform: rotate(45deg);
transition: .2s all;
}
label {
position: absolute;
color: $placeholder-color;
left: 1rem;
right: auto;
z-index: 2;
top: 50%;
transform: translateY(-50%);
background-color: #fff;
transition: .2s all, .1s opacity;
display: inline-block;
pointer-events: none;
}
input, &-input {
--border-width: 1px;
--border-width-top: 2px;
border: var(--border-width) solid #DADCE0;
border-radius: $border-radius-medium;
//padding: 0 1rem;
padding: calc(1rem - var(--border-width-top)) calc(1rem - var(--border-width));
box-sizing: border-box;
width: 100%;
min-height: 54px;
transition: .2s border-color;
position: relative;
z-index: 1;
//line-height: calc(54px - var(--border-width));
/* overflow: hidden;
white-space: nowrap; */
html.no-touch & {
&:hover:not(:focus):not(.error):not(.valid) {
border-color: var(--color-gray);
}
}
@include respond-to(handhelds) {
height: 50px;
}
/* font-weight: 500; */
/* &:hover {
border-color: #000;
} */
&:focus {
--border-width: 2px;
--border-width-top: 3px;
border-color: $button-primary-background;
//padding: 0 calc(1rem - 1px);
}
&:disabled {
background-color: #fff;
color: #000;
}
&.error {
border-color: $color-error;
& + label {
color: $color-error!important;
}
}
&.valid {
border-color: #26962F;
& + label {
color: #26962F !important;
}
}
/* &.error, &.valid {
transition: .2s border-width;
} */
&:focus ~ .arrow-down {
margin-top: -4px;
transform: rotate(225deg);
-webkit-transform: rotate(225deg);
border-color: $button-primary-background;
}
&:focus + label {
color: $button-primary-background;
}
&:focus + label, &:valid + label, &:not(:empty) + label, &:disabled + label {
top: -.5rem;
transform: none;
padding: 0 5px;
left: .75rem;
font-size: 0.75rem!important;
//color: #666;
opacity: 1;
}
}
}
.input-wrapper > * + * {
margin-top: 1.5rem;
}
::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
color: #909192;
opacity: 1; /* Firefox */
}
:-ms-input-placeholder { /* Internet Explorer 10-11 */
color: #a2acb4;
}
::-ms-input-placeholder { /* Microsoft Edge */
color: #a2acb4;
}
input:focus, button:focus {
outline: none;
}

2
src/scss/partials/pages/_authCode.scss

@ -9,7 +9,7 @@
justify-content: center; justify-content: center;
h4 { h4 {
&[contenteditable="true"] { &[contenteditable] {
padding: 0 1rem; padding: 0 1rem;
border: none; border: none;
outline: none; outline: none;

9
src/scss/partials/popups/_createPoll.scss

@ -25,9 +25,8 @@
.btn-icon { .btn-icon {
position: absolute; position: absolute;
right: .5rem; right: .5rem;
top: 50%; top: .5rem;
z-index: 1; z-index: 1;
transform: translateY(-50%);
opacity: 1; opacity: 1;
transition: opacity .2s ease; transition: opacity .2s ease;
} }
@ -47,7 +46,7 @@
.poll-create-questions { .poll-create-questions {
padding: 0px 1.25rem 2.03125rem; padding: 0px 1.25rem 2.03125rem;
.input-field input { .input-field-input {
padding-right: 3.25rem; padding-right: 3.25rem;
} }
} }
@ -65,4 +64,8 @@
font-size: .875rem; font-size: .875rem;
line-height: 1.2; line-height: 1.2;
} }
.btn-primary {
width: 94px;
}
} }

15
src/scss/partials/popups/_mediaAttacher.scss

@ -132,27 +132,22 @@
margin-top: 25px; margin-top: 25px;
flex: 0 0 auto; flex: 0 0 auto;
&::placeholder { &-input {
color: #a2acb4;
}
input {
//height: 54px; //height: 54px;
font-size: 1rem; font-size: 1rem;
border-radius: $border-radius-medium; border-radius: $border-radius-medium;
&:not(:focus):empty + label {
opacity: 0;
}
} }
label { label {
font-size: inherit; font-size: inherit;
opacity: 0;
} }
} }
} }
.popup-create-poll.popup-new-media .btn-primary {
width: 94px;
}
.popup-new-media.popup-send-photo { .popup-new-media.popup-send-photo {
.popup-header { .popup-header {
padding: 0; padding: 0;

167
src/scss/style.scss

@ -102,6 +102,7 @@ $messages-container-width: 728px;
} }
@import "partials/ico"; @import "partials/ico";
@import "partials/input";
@import "partials/button"; @import "partials/button";
@import "partials/checkbox"; @import "partials/checkbox";
@import "partials/chatlist"; @import "partials/chatlist";
@ -294,7 +295,7 @@ h4 {
} }
} }
input { input, [contenteditable] {
caret-color: $button-primary-background; caret-color: $button-primary-background;
} }
@ -442,140 +443,6 @@ hr {
} }
} }
.input-wrapper {
width: 360px;
margin: 0 auto;
}
.input-field {
position: relative;
.arrow-down {
position: absolute;
content: " ";
top: 50%;
bottom: 0;
right: 21px;
cursor: pointer;
height: 0;
width: 0;
border: solid #707579;
border-radius: 1px;
border-width: 0 2px 2px 0;
display: inline-block;
padding: 5px;
vertical-align: middle;
z-index: 2;
margin-top: -9px;
transform: rotate(45deg);
-webkit-transform: rotate(45deg);
transition: .2s all;
}
label {
position: absolute;
color: $placeholder-color;
left: 1rem;
right: auto;
z-index: 2;
top: 50%;
transform: translateY(-50%);
background-color: #fff;
transition: .2s all, .1s opacity;
display: inline-block;
cursor: text;
}
input {
--border-width: 1px;
border: var(--border-width) solid #DADCE0;
border-radius: $border-radius-medium;
//padding: 0 1rem;
padding: 0 calc(1rem - var(--border-width));
box-sizing: border-box;
width: 100%;
height: 54px;
transition: .2s border-color;
position: relative;
z-index: 1;
html.no-touch & {
&:hover:not(:focus):not(.error):not(.valid) {
border-color: var(--color-gray);
}
}
@include respond-to(handhelds) {
height: 50px;
}
/* font-weight: 500; */
/* &:hover {
border-color: #000;
} */
&:focus {
--border-width: 2px;
border-color: $button-primary-background;
//padding: 0 calc(1rem - 1px);
}
&:disabled {
background-color: #fff;
color: #000;
}
&.error {
border-color: $color-error;
& + label {
color: $color-error!important;
}
}
&.valid {
border-color: #26962F;
& + label {
color: #26962F !important;
}
}
/* &.error, &.valid {
transition: .2s border-width;
} */
&:focus ~ .arrow-down {
margin-top: -4px;
transform: rotate(225deg);
-webkit-transform: rotate(225deg);
border-color: $button-primary-background;
}
&:focus + label {
color: $button-primary-background;
}
&:focus + label, &:valid + label, &:disabled + label {
top: -.5rem;
transform: none;
padding: 0 5px;
left: .75rem;
font-size: 0.75rem!important;
//color: #666;
opacity: 1;
}
}
}
.input-wrapper > * + * {
margin-top: 1.5rem;
}
.select-wrapper { .select-wrapper {
max-height: 23.5rem; max-height: 23.5rem;
/* height: auto; */ /* height: auto; */
@ -640,23 +507,6 @@ hr {
text-align: right; text-align: right;
} }
::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
color: #909192;
opacity: 1; /* Firefox */
}
:-ms-input-placeholder { /* Internet Explorer 10-11 */
color: #a2acb4;
}
::-ms-input-placeholder { /* Microsoft Edge */
color: #a2acb4;
}
input:focus, button:focus {
outline: none;
}
// this dimensions will be used for monkey business // this dimensions will be used for monkey business
.auth-image { .auth-image {
width: 166px; width: 166px;
@ -787,8 +637,19 @@ img.emoji {
} }
} }
[contenteditable=true] { [contenteditable] {
user-select: text; user-select: text;
outline: none;
cursor: text;
&[data-placeholder] {
&:empty:before {
content: attr(data-placeholder);
color: #a2acb4;
display: block; /* For Firefox By Ariel Flesler */
cursor: text;
}
}
} }
.sticky_sentinel { .sticky_sentinel {

Loading…
Cancel
Save