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. 106
      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. 21
      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"; @@ -11,7 +11,7 @@ import apiManager from "../../lib/mtproto/mtprotoworker";
import opusDecodeController from "../../lib/opusDecodeController";
import { RichTextProcessor } from "../../lib/richtextprocessor";
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 emoticonsDropdown from "../emoticonsDropdown";
import PopupCreatePoll from "../popupCreatePoll";
@ -21,7 +21,7 @@ import { ripple } from '../ripple'; @@ -21,7 +21,7 @@ import { ripple } from '../ripple';
import Scrollable from "../scrollable";
import { toast } from "../toast";
import { wrapReply } from "../wrappers";
import { checkRTL } from '../../helpers/string';
import InputField from '../inputField';
const RECORD_MIN_TIME = 500;
const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this group.';
@ -30,7 +30,7 @@ type ChatInputHelperType = 'edit' | 'webpage' | 'forward' | 'reply'; @@ -30,7 +30,7 @@ type ChatInputHelperType = 'edit' | 'webpage' | 'forward' | 'reply';
export class ChatInput {
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 inputMessageContainer = document.getElementsByClassName('input-message-container')[0] as HTMLDivElement;
public inputScroll = new Scrollable(this.inputMessageContainer);
@ -73,6 +73,15 @@ export class ChatInput { @@ -73,6 +73,15 @@ export class ChatInput {
private helperFunc: () => void;
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;
let willAttachType: 'document' | 'media';
@ -183,8 +192,6 @@ export class ChatInput { @@ -183,8 +192,6 @@ export class ChatInput {
this.messageInput.addEventListener('input', (e) => {
//console.log('messageInput input', this.messageInput.innerText, this.serializeNodes(Array.from(this.messageInput.childNodes)));
this.setDirection();
const value = this.messageInput.innerText;
const entities = RichTextProcessor.parseEntities(value);
@ -217,7 +224,7 @@ export class ChatInput { @@ -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 = '';
appMessagesManager.setTyping($rootScope.selectedPeerID, 'sendMessageCancelAction');
@ -232,7 +239,7 @@ export class ChatInput { @@ -232,7 +239,7 @@ export class ChatInput {
this.updateSendBtn();
});
if(!RichTextProcessor.emojiSupported) {
/* if(!RichTextProcessor.emojiSupported) {
this.messageInput.addEventListener('copy', (e) => {
const selection = document.getSelection();
@ -243,7 +250,7 @@ export class ChatInput { @@ -243,7 +250,7 @@ export class ChatInput {
let selectedNodes = Array.from(ancestorContainer.childNodes).slice(range.startOffset, range.endOffset);
if(selectedNodes.length) {
str = this.serializeNodes(selectedNodes);
str = serializeNodes(selectedNodes);
} else {
str = selection.toString();
}
@ -257,30 +264,7 @@ export class ChatInput { @@ -257,30 +264,7 @@ export class ChatInput {
event.clipboardData.setData('text/plain', str);
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) => {
let files = (e.target as HTMLInputElement & EventTarget).files;
@ -292,7 +276,7 @@ export class ChatInput { @@ -292,7 +276,7 @@ export class ChatInput {
this.fileInput.value = '';
}, false);
document.addEventListener('paste', (event) => {
document.addEventListener('paste', (e) => {
const peerID = $rootScope.selectedPeerID;
if(!peerID || $rootScope.overlayIsActive || (peerID < 0 && !appChatsManager.hasRights(peerID, 'send', 'send_media'))) {
return;
@ -301,13 +285,15 @@ export class ChatInput { @@ -301,13 +285,15 @@ export class ChatInput {
//console.log('document paste');
// @ts-ignore
var items = (event.clipboardData || event.originalEvent.clipboardData).items;
const items = (e.clipboardData || e.originalEvent.clipboardData).items;
//console.log('item', event.clipboardData.getData());
let foundFile = false;
for(let i = 0; i < items.length; ++i) {
if(items[i].kind == 'file') {
event.preventDefault()
event.cancelBubble = true;
event.stopPropagation();
e.preventDefault()
e.cancelBubble = true;
e.stopPropagation();
foundFile = true;
let file = items[i].getAsFile();
//console.log(items[i], file);
@ -541,10 +527,8 @@ export class ChatInput { @@ -541,10 +527,8 @@ export class ChatInput {
});
}
private isInputEmpty() {
let value = this.messageInput.innerText;
return !value.trim() && !this.serializeNodes(Array.from(this.messageInput.childNodes)).trim();
public isInputEmpty() {
return isInputEmpty(this.messageInput);
}
public updateSendBtn() {
@ -556,18 +540,6 @@ export class ChatInput { @@ -556,18 +540,6 @@ export class ChatInput {
this.btnSend.classList.toggle('send', icon == 'send');
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) {
let dialog = appMessagesManager.getDialogByPeerID(appImManager.peerID)[0];
@ -587,17 +559,6 @@ export class ChatInput { @@ -587,17 +559,6 @@ export class ChatInput {
}
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() {
@ -708,7 +669,6 @@ export class ChatInput { @@ -708,7 +669,6 @@ export class ChatInput {
this.editMsgID = 0;
this.helperType = this.helperFunc = undefined;
this.chatInput.parentElement.classList.remove('is-helper-active');
this.setDirection();
}
public setTopInfo(type: ChatInputHelperType, callerFunc: () => void, title = '', subtitle = '', input?: string, message?: any) {
@ -735,7 +695,6 @@ export class ChatInput { @@ -735,7 +695,6 @@ export class ChatInput {
setTimeout(() => {
this.updateSendBtn();
this.setDirection();
}, 0);
}

106
src/components/inputField.ts

@ -1,33 +1,119 @@ @@ -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');
div.classList.add('input-field');
div.innerHTML = `
<input type="text" name="${name}" id="input-${name}" placeholder="${placeholder}" autocomplete="off" required="">
<label for="input-${name}">${label}</label>
`;
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 = `
<input type="text" name="${name}" id="input-${name}" ${placeholder ? `placeholder="${placeholder}"` : ''} autocomplete="off" required="" class="input-field-input">
${label ? `<label for="input-${name}">${label}</label>` : ''}
`;
}
let processInput: () => void;
if(maxLength) {
const input = div.firstElementChild as HTMLInputElement;
const labelEl = div.lastElementChild as HTMLLabelElement;
let showingLength = false;
input.addEventListener('input', (e) => {
processInput = () => {
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;
input.classList.toggle('error', isError);
if(isError || diff <= showLengthOn) {
labelEl.innerText = label + ` (${maxLength - input.value.length})`;
labelEl.innerText = label + ` (${maxLength - inputLength})`;
if(!showingLength) showingLength = true;
} else if((wasError && !isError) || showingLength) {
labelEl.innerText = label;
showingLength = false;
}
});
};
input.addEventListener('input', processInput);
}
return div;
return {container: div, input: div.firstElementChild as HTMLInputElement};
};
export default InputField;

51
src/components/popupCreatePoll.ts

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

16
src/components/popupNewMedia.ts

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

4
src/components/scrollable.ts

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

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

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

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

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

22
src/helpers/dom.ts

@ -127,6 +127,28 @@ export function getRichElementValue(node: any, lines: string[], line: string[], @@ -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 &&
typeof window.requestAnimationFrame == 'function') {
window.onAnimationFrameCallback = function (cb) {

34
src/index.hbs

@ -168,7 +168,7 @@ @@ -168,7 +168,7 @@
</div>
</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-item menu-channel tgico-newchannel rp">New Channel</div>
<div class="btn-menu-item menu-group tgico-newgroup rp">New Group</div>
@ -219,7 +219,7 @@ @@ -219,7 +219,7 @@
</div>
</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 class="sidebar-slider-item addmembers-container">
@ -228,7 +228,7 @@ @@ -228,7 +228,7 @@
<div class="sidebar-header__title">Add Members</div>
</div>
<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 class="sidebar-slider-item new-group-container">
@ -247,7 +247,7 @@ @@ -247,7 +247,7 @@
<label for="new-group-name">Group Name</label>
</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 class="sidebar-slider-item settings-container">
@ -287,34 +287,14 @@ @@ -287,34 +287,14 @@
<canvas class="avatar-edit-canvas"></canvas>
<span class="tgico tgico-cameraadd"></span>
</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>
<hr/>
<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:
<br><a class="profile-url" href="#" target="_blank"></a></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 class="sidebar-slider-item chat-folders-container">
@ -447,9 +427,7 @@ @@ -447,9 +427,7 @@
<div class="new-message-wrapper">
<button class="btn-icon rp tgico toggle-emoticons" id="toggle-emoticons"></button>
<!-- <textarea type="text" id="input-message" placeholder="Message" contenteditable="true"></textarea> -->
<div class="input-message-container">
<div id="input-message" contenteditable="true" data-placeholder="Message"></div>
</div>
<div class="input-message-container"></div>
<button class="btn-icon tgico-attach btn-menu-toggle" id="attach-file"></button>
<div class="record-time"></div>
<input type="file" id="input-file" style="display: none;" multiple />

14
src/lib/appManagers/appChatsManager.ts

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

2
src/lib/appManagers/appUsersManager.ts

@ -242,7 +242,7 @@ export class AppUsersManager { @@ -242,7 +242,7 @@ export class AppUsersManager {
const fullName = user.first_name + ' ' + (user.last_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;
} else {
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 { @@ -809,15 +809,15 @@ namespace RichTextProcessor {
let first = '', last = '';
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;
if(onlyFirst) return first;
if(str.indexOf(' ') !== -1) {
const lastNode = childNodes[childNodes.length - 1];
if(lastNode == firstNode) last = lastNode.textContent.split(' ').pop().charAt(0).toUpperCase();
else if('length' in lastNode) last = (lastNode as any).textContent.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.trim().charAt(0).toUpperCase();
else last = (lastNode as HTMLElement).outerHTML;
}

21
src/scss/partials/_button.scss

@ -42,6 +42,18 @@ @@ -42,6 +42,18 @@
transform: translate3d(0, var(--translateY), 0);
z-index: 3;
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 {
--translateY: 0;
@ -49,7 +61,12 @@ @@ -49,7 +61,12 @@
body.animation-level-0 & {
transition: none !important;
}
}
&:disabled {
opacity: 1 !important;
pointer-events: all !important;
}
}
.btn-menu {
@ -238,7 +255,7 @@ @@ -238,7 +255,7 @@
cursor: pointer !important;
pointer-events: all !important;
&:not(.btn-primary).menu-open {
&:not(.btn-primary):not(.btn-corner).menu-open {
background-color: var(--color-gray-hover);
}

9
src/scss/partials/_chat.scss

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

155
src/scss/partials/_input.scss

@ -0,0 +1,155 @@ @@ -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 @@ @@ -9,7 +9,7 @@
justify-content: center;
h4 {
&[contenteditable="true"] {
&[contenteditable] {
padding: 0 1rem;
border: none;
outline: none;

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

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

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

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

167
src/scss/style.scss

@ -102,6 +102,7 @@ $messages-container-width: 728px; @@ -102,6 +102,7 @@ $messages-container-width: 728px;
}
@import "partials/ico";
@import "partials/input";
@import "partials/button";
@import "partials/checkbox";
@import "partials/chatlist";
@ -294,7 +295,7 @@ h4 { @@ -294,7 +295,7 @@ h4 {
}
}
input {
input, [contenteditable] {
caret-color: $button-primary-background;
}
@ -442,140 +443,6 @@ hr { @@ -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 {
max-height: 23.5rem;
/* height: auto; */
@ -640,23 +507,6 @@ hr { @@ -640,23 +507,6 @@ hr {
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
.auth-image {
width: 166px;
@ -787,8 +637,19 @@ img.emoji { @@ -787,8 +637,19 @@ img.emoji {
}
}
[contenteditable=true] {
[contenteditable] {
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 {

Loading…
Cancel
Save