way improved editing of messages

This commit is contained in:
MathMan05 2024-11-25 14:39:38 -06:00
parent c1645099b8
commit ce538b3909
8 changed files with 204 additions and 145 deletions

View file

@ -791,6 +791,15 @@ class Channel extends SnowFlake{
return new Message(json[0], this); return new Message(json[0], this);
} }
} }
editLast(){
let message:Message|undefined=this.lastmessage;
while(message&&message.author!==this.localuser.user){
message=this.messages.get(this.idToPrev.get(message.id) as string);
}
if(message){
message.setEdit();
}
}
static genid: number = 0; static genid: number = 0;
async getHTML(){ async getHTML(){
const id = ++Channel.genid; const id = ++Channel.genid;
@ -842,6 +851,7 @@ class Channel extends SnowFlake{
await this.buildmessages(); await this.buildmessages();
//loading.classList.remove("loading"); //loading.classList.remove("loading");
(document.getElementById("typebox") as HTMLDivElement).contentEditable =""+this.canMessage; (document.getElementById("typebox") as HTMLDivElement).contentEditable =""+this.canMessage;
(document.getElementById("typebox") as HTMLDivElement).focus();
} }
typingmap: Map<Member, number> = new Map(); typingmap: Map<Member, number> = new Map();
async typingStart(typing: startTypingjson): Promise<void>{ async typingStart(typing: startTypingjson): Promise<void>{

View file

@ -197,6 +197,7 @@ class Group extends Channel{
} }
this.buildmessages(); this.buildmessages();
(document.getElementById("typebox") as HTMLDivElement).contentEditable ="" + true; (document.getElementById("typebox") as HTMLDivElement).contentEditable ="" + true;
(document.getElementById("typebox") as HTMLDivElement).focus();
} }
messageCreate(messagep: { d: messagejson }){ messageCreate(messagep: { d: messagejson }){
const messagez = new Message(messagep.d, this); const messagez = new Message(messagep.d, this);

View file

@ -75,7 +75,7 @@
</div> </div>
</div> </div>
<div style="position: relative;"> <div style="position: relative;">
<div id="searchOptions" class="flexttb"></div> <div id="searchOptions" class="flexttb searchOptions"></div>
</div> </div>
<div id="pasteimage" class="flexltr"></div> <div id="pasteimage" class="flexltr"></div>
<div id="replybox" class="hideReplyBox"></div> <div id="replybox" class="hideReplyBox"></div>

View file

@ -153,36 +153,30 @@ import { I18n } from "./i18n.js";
if(thisUser.keyup(event)){return} if(thisUser.keyup(event)){return}
const channel = thisUser.channelfocus; const channel = thisUser.channelfocus;
if(!channel)return; if(!channel)return;
if(markdown.rawString===""&&event.key==="ArrowUp"){
channel.editLast();
return;
}
channel.typingstart(); channel.typingstart();
if(event.key === "Enter" && !event.shiftKey){ if(event.key === "Enter" && !event.shiftKey){
event.preventDefault(); event.preventDefault();
replyingTo = thisUser.channelfocus? thisUser.channelfocus.replyingto: null;
if(channel.editing){ if(replyingTo?.div){
channel.editing.edit(markdown.rawString); replyingTo.div.classList.remove("replying");
channel.editing = null; }
}else{ if(thisUser.channelfocus){
replyingTo = thisUser.channelfocus thisUser.channelfocus.replyingto = null;
? thisUser.channelfocus.replyingto }
: null; channel.sendMessage(markdown.rawString, {
if(replyingTo?.div){ attachments: images,
replyingTo.div.classList.remove("replying"); // @ts-ignore This is valid according to the API
} embeds: [], // Add an empty array for the embeds property
if(thisUser.channelfocus){ replyingto: replyingTo,
thisUser.channelfocus.replyingto = null; });
} if(thisUser.channelfocus){
channel.sendMessage(markdown.rawString, { thisUser.channelfocus.makereplybox();
attachments: images,
// @ts-ignore This is valid according to the API
embeds: [], // Add an empty array for the embeds property
replyingto: replyingTo,
});
if(thisUser.channelfocus){
thisUser.channelfocus.makereplybox();
}
} }
while(images.length){ while(images.length){
images.pop(); images.pop();
pasteImageElement.removeChild(imagesHtml.pop() as HTMLElement); pasteImageElement.removeChild(imagesHtml.pop() as HTMLElement);

View file

@ -1717,34 +1717,32 @@ class Localuser{
Bot.InviteMaker(appId,form,this.info); Bot.InviteMaker(appId,form,this.info);
}) })
} }
typeMd?:MarkDown;
readonly autofillregex=Object.freeze(/[@#:]([a-z0-9 ]*)$/i); readonly autofillregex=Object.freeze(/[@#:]([a-z0-9 ]*)$/i);
mdBox(){ mdBox(){
interface CustomHTMLDivElement extends HTMLDivElement {markdown: MarkDown;} interface CustomHTMLDivElement extends HTMLDivElement {markdown: MarkDown;}
const typebox = document.getElementById("typebox") as CustomHTMLDivElement; const typebox = document.getElementById("typebox") as CustomHTMLDivElement;
this.typeMd=typebox.markdown; const typeMd=typebox.markdown;
this.typeMd.owner=this; typeMd.owner=this;
this.typeMd.onUpdate=this.search.bind(this); typeMd.onUpdate=(str,pre)=>{
this.search(document.getElementById("searchOptions") as HTMLDivElement,typeMd,str,pre);
}
} }
MDReplace(replacewith:string,original:string){ MDReplace(replacewith:string,original:string,typebox:MarkDown){
const typebox = document.getElementById("typebox") as HTMLDivElement; let raw=typebox.rawString;
if(!this.typeMd)return;
let raw=this.typeMd.rawString;
raw=raw.split(original)[1]; raw=raw.split(original)[1];
if(raw===undefined) return; if(raw===undefined) return;
raw=original.replace(this.autofillregex,"")+replacewith+raw; raw=original.replace(this.autofillregex,"")+replacewith+raw;
console.log(raw); console.log(raw);
console.log(replacewith); console.log(replacewith);
console.log(original); console.log(original);
this.typeMd.txt = raw.split(""); typebox.txt = raw.split("");
const match=original.match(this.autofillregex); const match=original.match(this.autofillregex);
if(match){ if(match){
this.typeMd.boxupdate(typebox,replacewith.length-match[0].length); typebox.boxupdate(replacewith.length-match[0].length);
} }
} }
MDSearchOptions(options:[string,string,void|HTMLElement][],original:string){ MDSearchOptions(options:[string,string,void|HTMLElement][],original:string,div:HTMLDivElement,typebox:MarkDown){
const div=document.getElementById("searchOptions");
if(!div)return; if(!div)return;
div.innerHTML=""; div.innerHTML="";
let i=0; let i=0;
@ -1757,7 +1755,6 @@ class Localuser{
const span=document.createElement("span"); const span=document.createElement("span");
htmloptions.push(span); htmloptions.push(span);
if(thing[2]){ if(thing[2]){
console.log(thing);
span.append(thing[2]); span.append(thing[2]);
} }
@ -1766,19 +1763,21 @@ class Localuser{
if(e){ if(e){
const selection = window.getSelection() as Selection; const selection = window.getSelection() as Selection;
const typebox = document.getElementById("typebox") as HTMLDivElement; const box=typebox.box.deref();
if(!box) return;
if(selection){ if(selection){
console.warn(original); console.warn(original);
const pos = getTextNodeAtPosition(typebox, original.length-(original.match(this.autofillregex) as RegExpMatchArray)[0].length+thing[1].length);
const pos = getTextNodeAtPosition(box, original.length-(original.match(this.autofillregex) as RegExpMatchArray)[0].length+thing[1].length);
selection.removeAllRanges(); selection.removeAllRanges();
const range = new Range(); const range = new Range();
range.setStart(pos.node, pos.position); range.setStart(pos.node, pos.position);
selection.addRange(range); selection.addRange(range);
} }
e.preventDefault(); e.preventDefault();
typebox.focus(); box.focus();
} }
this.MDReplace(thing[1],original); this.MDReplace(thing[1],original,typebox);
div.innerHTML=""; div.innerHTML="";
remove(); remove();
} }
@ -1837,7 +1836,7 @@ class Localuser{
remove(); remove();
} }
} }
MDFindChannel(name:string,orginal:string){ MDFindChannel(name:string,orginal:string,box:HTMLDivElement,typebox:MarkDown){
const maybe:[number,Channel][]=[]; const maybe:[number,Channel][]=[];
if(this.lookingguild&&this.lookingguild.id!=="@me"){ if(this.lookingguild&&this.lookingguild.id!=="@me"){
for(const channel of this.lookingguild.channels){ for(const channel of this.lookingguild.channels){
@ -1848,7 +1847,7 @@ class Localuser{
} }
} }
maybe.sort((a,b)=>b[0]-a[0]); maybe.sort((a,b)=>b[0]-a[0]);
this.MDSearchOptions(maybe.map((a)=>["# "+a[1].name,`<#${a[1].id}> `,undefined]),orginal); this.MDSearchOptions(maybe.map((a)=>["# "+a[1].name,`<#${a[1].id}> `,undefined]),orginal,box,typebox);
} }
async getUser(id:string){ async getUser(id:string){
if(this.userMap.has(id)){ if(this.userMap.has(id)){
@ -1856,7 +1855,7 @@ class Localuser{
} }
return new User(await (await fetch(this.info.api+"/users/"+id)).json(),this); return new User(await (await fetch(this.info.api+"/users/"+id)).json(),this);
} }
MDFineMentionGen(name:string,original:string){ MDFineMentionGen(name:string,original:string,box:HTMLDivElement,typebox:MarkDown){
let members:[Member,number][]=[]; let members:[Member,number][]=[];
if(this.lookingguild){ if(this.lookingguild){
for(const member of this.lookingguild.members){ for(const member of this.lookingguild.members){
@ -1867,11 +1866,11 @@ class Localuser{
} }
} }
members.sort((a,b)=>b[1]-a[1]); members.sort((a,b)=>b[1]-a[1]);
this.MDSearchOptions(members.map((a)=>["@"+a[0].name,`<@${a[0].id}> `,undefined]),original); this.MDSearchOptions(members.map((a)=>["@"+a[0].name,`<@${a[0].id}> `,undefined]),original,box,typebox);
} }
MDFindMention(name:string,original:string){ MDFindMention(name:string,original:string,box:HTMLDivElement,typebox:MarkDown){
if(this.ws&&this.lookingguild){ if(this.ws&&this.lookingguild){
this.MDFineMentionGen(name,original); this.MDFineMentionGen(name,original,box,typebox);
const nonce=Math.floor(Math.random()*10**8)+""; const nonce=Math.floor(Math.random()*10**8)+"";
if(this.lookingguild.member_count<=this.lookingguild.members.size) return; if(this.lookingguild.member_count<=this.lookingguild.members.size) return;
this.ws.send(JSON.stringify( this.ws.send(JSON.stringify(
@ -1906,19 +1905,19 @@ class Localuser{
await Member.new(thing,this.lookingguild as Guild) await Member.new(thing,this.lookingguild as Guild)
} }
} }
this.MDFineMentionGen(name,original); this.MDFineMentionGen(name,original,box,typebox);
} }
}) })
} }
} }
findEmoji(search:string,orginal:string){ findEmoji(search:string,orginal:string,box:HTMLDivElement,typebox:MarkDown){
const emj=Emoji.searchEmoji(search,this,10); const emj=Emoji.searchEmoji(search,this,10);
const map=emj.map(([emoji]):[string,string,HTMLElement]=>{ const map=emj.map(([emoji]):[string,string,HTMLElement]=>{
return [emoji.name,emoji.id?`<${emoji.animated?"a":""}:${emoji.name}:${emoji.id}>`:emoji.emoji as string,emoji.getHTML()] return [emoji.name,emoji.id?`<${emoji.animated?"a":""}:${emoji.name}:${emoji.id}>`:emoji.emoji as string,emoji.getHTML()]
}) })
this.MDSearchOptions(map,orginal); this.MDSearchOptions(map,orginal,box,typebox);
} }
search(str:string,pre:boolean){ search(box:HTMLDivElement,md:MarkDown,str:string,pre:boolean){
if(!pre){ if(!pre){
const match=str.match(this.autofillregex); const match=str.match(this.autofillregex);
@ -1926,25 +1925,23 @@ class Localuser{
const [type, search]=[match[0][0],match[0].split(/@|#|:/)[1]]; const [type, search]=[match[0][0],match[0].split(/@|#|:/)[1]];
switch(type){ switch(type){
case "#": case "#":
this.MDFindChannel(search,str); this.MDFindChannel(search,str,box,md);
break; break;
case "@": case "@":
this.MDFindMention(search,str); this.MDFindMention(search,str,box,md);
break; break;
case ":": case ":":
if(search.length>=2){ if(search.length>=2){
this.findEmoji(search,str) this.findEmoji(search,str,box,md)
}else{ }else{
this.MDSearchOptions([],""); this.MDSearchOptions([],"",box,md);
} }
break; break;
} }
return return
} }
} }
const div=document.getElementById("searchOptions"); box.innerHTML="";
if(!div)return;
div.innerHTML="";
} }
keydown:(event:KeyboardEvent)=>unknown=()=>{}; keydown:(event:KeyboardEvent)=>unknown=()=>{};
keyup:(event:KeyboardEvent)=>boolean=()=>false; keyup:(event:KeyboardEvent)=>boolean=()=>false;

View file

@ -686,7 +686,9 @@ txt[j + 1] === undefined)
e.target.classList.add("unspoiled"); e.target.classList.add("unspoiled");
} }
onUpdate:(upto:string,pre:boolean)=>unknown=()=>{}; onUpdate:(upto:string,pre:boolean)=>unknown=()=>{};
box=new WeakRef(document.createElement("div"));
giveBox(box: HTMLDivElement,onUpdate:(upto:string,pre:boolean)=>unknown=()=>{}){ giveBox(box: HTMLDivElement,onUpdate:(upto:string,pre:boolean)=>unknown=()=>{}){
this.box=new WeakRef(box);
this.onUpdate=onUpdate; this.onUpdate=onUpdate;
box.onkeydown = _=>{ box.onkeydown = _=>{
//console.log(_); //console.log(_);
@ -697,7 +699,7 @@ txt[j + 1] === undefined)
if(content !== prevcontent){ if(content !== prevcontent){
prevcontent = content; prevcontent = content;
this.txt = content.split(""); this.txt = content.split("");
this.boxupdate(box); this.boxupdate();
MarkDown.gatherBoxText(box); MarkDown.gatherBoxText(box);
} }
@ -713,7 +715,9 @@ txt[j + 1] === undefined)
box.onkeyup(new KeyboardEvent("_")); box.onkeyup(new KeyboardEvent("_"));
}; };
} }
boxupdate(box: HTMLElement,offset=0){ boxupdate(offset=0){
const box=this.box.deref();
if(!box) return;
const restore = saveCaretPosition(box,offset); const restore = saveCaretPosition(box,offset);
box.innerHTML = ""; box.innerHTML = "";
box.append(this.makeHTML({ keep: true })); box.append(this.makeHTML({ keep: true }));
@ -832,85 +836,91 @@ let formatted=false;
function saveCaretPosition(context: HTMLElement,offset=0){ function saveCaretPosition(context: HTMLElement,offset=0){
const selection = window.getSelection() as Selection; const selection = window.getSelection() as Selection;
if(!selection)return; if(!selection)return;
const range = selection.getRangeAt(0); try{
let base=selection.anchorNode as Node; const range = selection.getRangeAt(0);
range.setStart(base, 0);
let baseString:string; let base=selection.anchorNode as Node;
if(!(base instanceof Text)){ range.setStart(base, 0);
let i=0; let baseString:string;
const index=selection.focusOffset; if(!(base instanceof Text)){
//@ts-ignore let i=0;
for(const thing of base.childNodes){ const index=selection.focusOffset;
if(i===index){ //@ts-ignore
base=thing; for(const thing of base.childNodes){
break; if(i===index){
base=thing;
break;
}
i++;
}
if(base instanceof HTMLElement){
baseString=MarkDown.gatherBoxText(base)
}else{
baseString=base.textContent as string;
} }
i++;
}
if(base instanceof HTMLElement){
baseString=MarkDown.gatherBoxText(base)
}else{ }else{
baseString=base.textContent as string; baseString=selection.toString();
} }
}else{
baseString=selection.toString();
}
range.setStart(context, 0); range.setStart(context, 0);
let build=""; let build="";
//I think this is working now :3 //I think this is working now :3
function crawlForText(context:Node){ function crawlForText(context:Node){
//@ts-ignore //@ts-ignore
const children=[...context.childNodes]; const children=[...context.childNodes];
if(children.length===1&&children[0] instanceof Text){ if(children.length===1&&children[0] instanceof Text){
if(selection.containsNode(context,false)){ if(selection.containsNode(context,false)){
build+=MarkDown.gatherBoxText(context as HTMLElement); build+=MarkDown.gatherBoxText(context as HTMLElement);
}else if(selection.containsNode(context,true)){ }else if(selection.containsNode(context,true)){
if(context.contains(base)||context===base||base.contains(context)){ if(context.contains(base)||context===base||base.contains(context)){
build+=baseString; build+=baseString;
}else{
build+=context.textContent;
}
}else{ }else{
build+=context.textContent; console.error(context);
} }
}else{ return;
console.error(context);
} }
return; for(const node of children as Node[]){
}
for(const node of children as Node[]){
if(selection.containsNode(node,false)){ if(selection.containsNode(node,false)){
if(node instanceof HTMLElement){ if(node instanceof HTMLElement){
build+=MarkDown.gatherBoxText(node); build+=MarkDown.gatherBoxText(node);
}else{
build+=node.textContent;
}
}else if(selection.containsNode(node,true)){
if(node instanceof HTMLElement){
crawlForText(node);
}else{
console.error(node,"This shouldn't happen")
}
}else{ }else{
build+=node.textContent; //console.error(node,"This shouldn't happen");
} }
}else if(selection.containsNode(node,true)){
if(node instanceof HTMLElement){
crawlForText(node);
}else{
console.error(node,"This shouldn't happen")
}
}else{
console.error(node,"This shouldn't happen");
} }
} }
crawlForText(context);
if(baseString==="\n"){
build+=baseString;
}
text=build;
let len=build.length+offset;
len=Math.min(len,MarkDown.gatherBoxText(context).length)
return function restore(){
if(!selection)return;
const pos = getTextNodeAtPosition(context, len);
selection.removeAllRanges();
const range = new Range();
range.setStart(pos.node, pos.position);
selection.addRange(range);
};
}catch{
return undefined;
} }
crawlForText(context);
if(baseString==="\n"){
build+=baseString;
}
text=build;
const len=build.length+offset;
return function restore(){
if(!selection)return;
const pos = getTextNodeAtPosition(context, len);
selection.removeAllRanges();
const range = new Range();
range.setStart(pos.node, pos.position);
selection.addRange(range);
};
} }
function getTextNodeAtPosition(root: Node, index: number):{ function getTextNodeAtPosition(root: Node, index: number):{

View file

@ -1,7 +1,7 @@
import{ Contextmenu }from"./contextmenu.js"; import{ Contextmenu }from"./contextmenu.js";
import{ User }from"./user.js"; import{ User }from"./user.js";
import{ Member }from"./member.js"; import{ Member }from"./member.js";
import{ MarkDown }from"./markdown.js"; import{ MarkDown, saveCaretPosition }from"./markdown.js";
import{ Embed }from"./embed.js"; import{ Embed }from"./embed.js";
import{ Channel }from"./channel.js"; import{ Channel }from"./channel.js";
import{ Localuser }from"./localuser.js"; import{ Localuser }from"./localuser.js";
@ -96,14 +96,10 @@ class Message extends SnowFlake{
); );
} }
setEdit(){ setEdit(){
const prev=this.channel.editing;
this.channel.editing = this; this.channel.editing = this;
const markdown = ( if(prev) prev.generateMessage();
document.getElementById("typebox") as HTMLDivElement & { this.generateMessage(undefined,false)
markdown: MarkDown;
}
).markdown as MarkDown;
markdown.txt = this.content.rawString.split("");
markdown.boxupdate(document.getElementById("typebox") as HTMLDivElement);
} }
constructor(messagejson: messagejson, owner: Channel){ constructor(messagejson: messagejson, owner: Channel){
super(messagejson.id); super(messagejson.id);
@ -340,6 +336,7 @@ class Message extends SnowFlake{
} }
generateMessage(premessage?: Message | undefined, ignoredblock = false){ generateMessage(premessage?: Message | undefined, ignoredblock = false){
if(!this.div)return; if(!this.div)return;
const editmode=this.channel.editing===this;
if(!premessage){ if(!premessage){
premessage = this.channel.messages.get( premessage = this.channel.messages.get(
this.channel.idToPrev.get(this.id) as string this.channel.idToPrev.get(this.id) as string
@ -476,8 +473,7 @@ class Message extends SnowFlake{
const newt = new Date(this.timestamp).getTime() / 1000; const newt = new Date(this.timestamp).getTime() / 1000;
current = newt - old > 600; current = newt - old > 600;
} }
const combine = const combine = premessage?.author != this.author || current || this.message_reference;
premessage?.author != this.author || current || this.message_reference;
if(combine){ if(combine){
const pfp = this.author.buildpfp(); const pfp = this.author.buildpfp();
this.author.bind(pfp, this.guild, false); this.author.bind(pfp, this.guild, false);
@ -526,13 +522,56 @@ class Message extends SnowFlake{
}else{ }else{
div.classList.remove("topMessage"); div.classList.remove("topMessage");
} }
const messaged = this.content.makeHTML();
(div as any).txt = messaged;
const messagedwrap = document.createElement("div"); const messagedwrap = document.createElement("div");
messagedwrap.classList.add("flexttb"); if(editmode){
messagedwrap.appendChild(messaged); const box=document.createElement("div");
text.appendChild(messagedwrap); box.classList.add("messageEditContainer");
const area=document.createElement("div");
const sb=document.createElement("div");
sb.style.position="absolute";
sb.style.width="100%";
const search=document.createElement("div");
search.classList.add("searchOptions","flexttb");
area.classList.add("editMessage");
area.contentEditable="true";
const md=new MarkDown(this.content.rawString,this.owner)
area.append(md.makeHTML());
area.addEventListener("keyup", (event)=>{
if(this.localuser.keyup(event)) return;
if(event.key === "Enter" && !event.shiftKey){
this.edit(md.rawString);
this.channel.editing=null;
this.generateMessage();
}
});
area.addEventListener("keydown", event=>{
this.localuser.keydown(event);
if(event.key === "Enter" && !event.shiftKey) event.preventDefault();
if(event.key === "Escape"){
this.channel.editing=null;
this.generateMessage();
}
});
md.giveBox(area,(str,pre)=>{
this.localuser.search(search,md,str,pre)
})
sb.append(search);
box.append(sb,area);
messagedwrap.append(box);
setTimeout(()=>{
area.focus();
const fun=saveCaretPosition(area,Infinity);
if(fun) fun();
})
}else{
this.content.onUpdate=()=>{};
const messaged = this.content.makeHTML();
(div as any).txt = messaged;
messagedwrap.classList.add("flexttb");
messagedwrap.appendChild(messaged);
}
text.appendChild(messagedwrap);
build.appendChild(text); build.appendChild(text);
if(this.attachments.length){ if(this.attachments.length){
console.log(this.attachments); console.log(this.attachments);

View file

@ -21,7 +21,7 @@ body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
#searchOptions{ .searchOptions{
padding:.05in .15in; padding:.05in .15in;
border-radius: .1in; border-radius: .1in;
background: var(--channels-bg); background: var(--channels-bg);
@ -46,15 +46,17 @@ body {
span:hover{ span:hover{
background:var(--button-bg); background:var(--button-bg);
} }
;
margin: 16px; margin: 16px;
border: solid .025in var(--black); border: solid .025in var(--black);
} }
#searchOptions:empty{ .searchOptions:empty{
padding: 0; padding: 0;
border: 0; border: 0;
} }
.messageEditContainer{
position: relative;
width:100%;
}
.flexgrow { .flexgrow {
flex-grow: 1; flex-grow: 1;
min-height: 0; min-height: 0;
@ -268,6 +270,11 @@ textarea {
transition: opacity .2s; transition: opacity .2s;
border: solid .03in var(--black); border: solid .03in var(--black);
} }
.editMessage{
background: var(--typebox-bg);
padding: .05in;
border-radius: .04in;
}
/* Animations */ /* Animations */
@keyframes fade { @keyframes fade {
0%, 100% { 0%, 100% {
@ -1004,6 +1011,7 @@ span.instanceStatus {
.commentrow { .commentrow {
word-break: break-word; word-break: break-word;
gap: 4px; gap: 4px;
width: 100%;
} }
.username { .username {
margin-top: auto; margin-top: auto;