2017-09-15 15:54:47 +03:00
function autosizeTextarea ( textarea , shadow ) {
shadow . style . width = textarea . clientWidth + "px" ;
shadow . value = textarea . value ;
textarea . style . height = shadow . scrollHeight + "px" ;
}
function queryArgsFromForm ( form ) {
const items = [ ] ;
for ( const { name , value } of form . elements ) {
if ( ! name ) continue ;
items . push ( encodeURIComponent ( name ) + '=' + encodeURIComponent ( value ) ) ;
}
return items . join ( '&' ) ;
}
2017-10-28 16:21:47 +03:00
function isEdited ( form ) {
for ( const { name , value , defaultValue } of form . elements ) {
if ( name && ( value !== defaultValue ) ) return true ;
}
return false ;
}
2017-11-24 19:04:36 +03:00
function loginDialog ( loginUrl ) {
2017-12-01 18:22:36 +03:00
const loginDialogHtml = "<div class=popup><div class=message><p>Your changes could not be saved</p><p>Log in and try again</p></div><div class=btn-row><button>Never mind</button> <a href='' target=login>Open login page</a></div></div>" ;
2017-11-24 19:04:36 +03:00
const dialog = document . createElement ( "div" ) ;
dialog . className = "modal-block" ;
dialog . innerHTML = loginDialogHtml ;
const loginLink = dialog . querySelector ( "a" ) ;
const dismiss = dialog . querySelector ( "button" ) ;
loginLink . setAttribute ( "href" , loginUrl ) ;
document . body . appendChild ( dialog ) ;
loginLink . focus ( ) ;
function remove ( ) {
document . body . removeChild ( dialog ) ;
}
return new Promise ( ( resolve , reject ) => {
loginLink . addEventListener ( "click" , ( ) => {
remove ( ) ;
resolve ( ) ;
} ) ;
dismiss . addEventListener ( "click" , ( ) => {
remove ( ) ;
resolve ( ) ;
} ) ;
} ) ;
}
2017-12-01 18:37:41 +03:00
function alertAsync ( message ) {
const dialogHtml = "<div class=popup><div class=message></div><div class=btn-row><button>OK</button></div></div>" ;
const dialog = document . createElement ( "div" ) ;
dialog . className = "modal-block" ;
dialog . innerHTML = dialogHtml ;
const messageNode = dialog . querySelector ( ".message" ) ;
const dismiss = dialog . querySelector ( "button" ) ;
messageNode . textContent = message ;
document . body . appendChild ( dialog ) ;
dismiss . focus ( ) ;
function remove ( ) {
document . body . removeChild ( dialog ) ;
}
return new Promise ( ( resolve , reject ) => {
dismiss . addEventListener ( "click" , ( ) => {
remove ( ) ;
resolve ( ) ;
} ) ;
} ) ;
}
2017-12-01 18:44:07 +03:00
function confirmAsync ( message ) {
const dialogHtml = "<div class=popup><div class=message></div><div class=btn-row><button name=no>No</button> <button name=yes>Yes</button></div></div>" ;
const dialog = document . createElement ( "div" ) ;
dialog . className = "modal-block" ;
dialog . innerHTML = dialogHtml ;
const messageNode = dialog . querySelector ( ".message" ) ;
const btnNo = dialog . querySelector ( 'button[name="no"]' ) ;
const btnYes = dialog . querySelector ( 'button[name="yes"]' ) ;
messageNode . textContent = message ;
document . body . appendChild ( dialog ) ;
btnNo . focus ( ) ;
function remove ( ) {
document . body . removeChild ( dialog ) ;
}
return new Promise ( ( resolve , reject ) => {
btnNo . addEventListener ( "click" , ( ) => {
remove ( ) ;
resolve ( false ) ;
} ) ;
btnYes . addEventListener ( "click" , ( ) => {
remove ( ) ;
resolve ( true ) ;
} ) ;
} ) ;
}
2017-09-15 15:54:47 +03:00
let hasBeenOpen = false ;
function openEditor ( ) {
2017-09-21 11:09:57 +03:00
const container = document . querySelector ( ".container" ) ;
const rendered = container . querySelector ( ".rendered" ) ;
const editor = container . querySelector ( ".editor" ) ;
2017-09-15 15:54:47 +03:00
const textarea = editor . querySelector ( 'textarea[name="body"]' ) ;
const shadow = editor . querySelector ( 'textarea.shadow-control' ) ;
const form = editor . querySelector ( "form" ) ;
const cancel = editor . querySelector ( '.cancel' ) ;
const footer = document . querySelector ( "footer" ) ;
const lastUpdated = footer . querySelector ( ".last-updated" ) ;
textarea . style . height = rendered . clientHeight + "px" ;
2017-09-21 11:09:57 +03:00
container . classList . add ( 'edit' ) ;
2017-09-15 15:54:47 +03:00
autosizeTextarea ( textarea , shadow ) ;
textarea . focus ( ) ;
if ( hasBeenOpen ) return ;
hasBeenOpen = true ;
textarea . addEventListener ( 'input' , ( ) => autosizeTextarea ( textarea , shadow ) ) ;
window . addEventListener ( 'resize' , ( ) => autosizeTextarea ( textarea , shadow ) ) ;
form . addEventListener ( "submit" , function ( ev ) {
ev . preventDefault ( ) ;
ev . stopPropagation ( ) ;
2017-09-21 13:18:09 +03:00
const body = queryArgsFromForm ( form ) ;
textarea . disabled = true ;
2017-11-24 19:04:36 +03:00
// TODO Disable other interaction as well: title editor, cancel and OK buttons
2017-09-21 13:18:09 +03:00
fetch (
form . getAttribute ( "action" ) ,
{
method : 'PUT' ,
headers : {
"Content-Type" : "application/x-www-form-urlencoded"
} ,
body : body ,
2017-09-26 14:39:44 +03:00
credentials : "same-origin" ,
2017-09-21 13:18:09 +03:00
}
) . then ( response => {
2017-11-29 14:53:41 +03:00
// I don't know how to more precisely determine that we hit a login redirect:
const probablyLoginRedirect = response . redirected &&
( response . headers . get ( "content-type" ) !== "application/json" ) ;
if ( probablyLoginRedirect ) {
return loginDialog ( response . url )
2017-11-24 19:04:36 +03:00
. then ( ( ) => {
textarea . disabled = false ;
} ) ;
2017-09-21 11:09:57 +03:00
}
2017-11-24 19:04:36 +03:00
if ( ! response . ok ) throw new Error ( "Unexpected status code (" + response . status + ")" ) ;
2017-11-20 18:37:52 +03:00
2017-11-24 19:04:36 +03:00
return response . json ( )
. then ( result => {
// Update url-bar, page title and footer
window . history . replaceState ( null , result . title , result . slug == "" ? "." : result . slug ) ;
document . querySelector ( "title" ) . textContent = result . title ;
lastUpdated . innerHTML = result . last _updated ;
lastUpdated . classList . remove ( "missing" ) ;
// Update body:
rendered . innerHTML = result . rendered ;
form . elements . title . value = result . title ;
shadow . value = textarea . value = result . body ;
// Update form:
form . elements . base _revision . value = result . revision ;
for ( const element of form . elements ) {
element . defaultValue = element . value ;
}
if ( ! result . conflict ) {
container . classList . remove ( 'edit' ) ;
}
textarea . disabled = false ;
autosizeTextarea ( textarea , shadow ) ;
if ( result . conflict ) {
2017-12-01 18:37:41 +03:00
return alertAsync ( "Your edit came into conflict with another change " +
"and has not been saved.\n" +
2017-11-24 19:04:36 +03:00
"Please resolve the merge conflict and save again." ) ;
}
} ) ;
2017-09-21 13:18:09 +03:00
} ) . catch ( err => {
2017-09-15 15:54:47 +03:00
textarea . disabled = false ;
console . error ( err ) ;
2017-12-01 18:37:41 +03:00
return alertAsync ( err . toString ( ) ) ;
2017-09-21 13:18:09 +03:00
} ) ;
2017-09-15 15:54:47 +03:00
} ) ;
cancel . addEventListener ( 'click' , function ( ev ) {
ev . preventDefault ( ) ;
ev . stopPropagation ( ) ;
2017-12-01 18:44:07 +03:00
Promise . resolve ( ! isEdited ( form ) || confirmAsync ( "Discard changes?" ) )
. then ( doReset => {
if ( doReset ) {
container . classList . remove ( 'edit' ) ;
form . reset ( ) ;
}
} ) ;
2017-09-15 15:54:47 +03:00
} ) ;
2017-10-28 16:26:50 +03:00
window . addEventListener ( "beforeunload" , function ( ev ) {
if ( isEdited ( form ) ) {
ev . preventDefault ( ) ;
return ev . returnValue = "Discard changes?" ;
}
} ) ;
2017-09-15 15:54:47 +03:00
}
document
. getElementById ( "openEditor" )
. addEventListener ( "click" , function ( ev ) {
ev . preventDefault ( ) ;
ev . stopPropagation ( ) ;
openEditor ( ) ;
} )
2017-10-02 18:08:59 +03:00
if ( document . querySelector ( ".container" ) . classList . contains ( "edit" ) ) {
openEditor ( ) ;
}