Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F5963323
edit.js
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
10 KB
Referenced Files
None
Subscribers
None
edit.js
View Options
/**
* In-progress edit recovery for action=edit
*/
'use strict'
;
const
storage
=
require
(
'./storage.js'
);
const
LoadNotification
=
require
(
'./LoadNotification.js'
);
const
pageName
=
mw
.
config
.
get
(
'wgPageName'
);
const
section
=
$
(
'input[name="wpSection"]'
).
val
()
||
null
;
const
inputFields
=
{};
const
fieldNamePrefix
=
'field_'
;
let
originalData
=
{};
let
changeDebounceTimer
=
null
;
// Number of miliseconds to debounce form input.
const
debounceTime
=
5000
;
// This module is loaded for every edit form, but not all should have Edit Recovery functioning.
const
wasPosted
=
mw
.
config
.
get
(
'wgEditRecoveryWasPosted'
);
const
isUndo
=
$
(
'input[name="wpUndoAfter"]'
).
length
>
0
;
const
isOldRevision
=
$
(
'input[name="oldid"]'
).
val
()
>
0
;
const
isConflict
=
mw
.
config
.
get
(
'wgEditMessage'
)
===
'editconflict'
;
const
useEditRecovery
=
!
isUndo
&&
!
isOldRevision
&&
!
isConflict
;
if
(
useEditRecovery
)
{
mw
.
hook
(
'wikipage.editform'
).
add
(
onLoadHandler
);
}
else
{
// Always remove the data-saved flag when editing without Edit Recovery.
// It may have been set by a previous editing session (within 5 minutes) that did use ER.
mw
.
storage
.
session
.
remove
(
'EditRecovery-data-saved'
);
}
const
windowManager
=
OO
.
ui
.
getWindowManager
();
windowManager
.
addWindows
(
[
new
mw
.
widgets
.
AbandonEditDialog
()
]
);
function
onLoadHandler
(
$editForm
)
{
mw
.
hook
(
'wikipage.editform'
).
remove
(
onLoadHandler
);
// Monitor all text-entry inputs for changes/typing.
const
inputsToMonitorSelector
=
'textarea, select, input:not([type="hidden"], [type="submit"])'
;
const
$inputsToMonitor
=
$editForm
.
find
(
inputsToMonitorSelector
);
$inputsToMonitor
.
each
(
(
_i
,
field
)
=>
{
if
(
field
.
classList
.
contains
(
'oo-ui-inputWidget-input'
)
)
{
try
{
inputFields
[
field
.
name
]
=
OO
.
ui
.
infuse
(
field
.
closest
(
'.oo-ui-widget'
)
);
}
catch
(
e
)
{
// Ignore any non-infusable widget because we won't be able to set its value.
}
}
else
{
inputFields
[
field
.
name
]
=
field
;
}
}
);
// Save the contents of all of those, as well as the following hidden inputs.
const
inputsToSaveNames
=
[
'wpSection'
,
'editRevId'
,
'oldid'
,
'parentRevId'
,
'format'
,
'model'
];
const
$inputsToSave
=
$editForm
.
find
(
'[name="'
+
inputsToSaveNames
.
join
(
'"], [name="'
)
+
'"]'
);
$inputsToSave
.
each
(
(
_i
,
field
)
=>
{
inputFields
[
field
.
name
]
=
field
;
}
);
// Store the original data for later comparing to the data-to-save. Use the defaultValue/defaultChecked in order to
// avoid using any data remembered by the browser. Note that we have to be careful to store with the same types as
// it will be done later, in order to correctly compare it (e.g. checkboxes as booleans).
Object
.
keys
(
inputFields
).
forEach
(
(
fieldName
)
=>
{
const
field
=
inputFields
[
fieldName
];
if
(
field
.
nodeName
===
'INPUT'
||
field
.
nodeName
===
'TEXTAREA'
)
{
if
(
field
.
type
===
'checkbox'
)
{
// Checkboxes (Minoredit and Watchthis are handled below as they are OOUI widgets).
originalData
[
fieldNamePrefix
+
fieldName
]
=
field
.
defaultChecked
;
}
else
{
// Other HTMLInputElements.
originalData
[
fieldNamePrefix
+
fieldName
]
=
field
.
defaultValue
;
}
}
else
if
(
field
.
$input
!==
undefined
)
{
// OOUI widgets, which may not have been infused by this point.
if
(
field
.
$input
[
0
].
type
===
'checkbox'
)
{
// Checkboxes.
originalData
[
fieldNamePrefix
+
fieldName
]
=
field
.
$input
[
0
].
defaultChecked
;
}
else
{
// Other OOUI widgets.
originalData
[
fieldNamePrefix
+
fieldName
]
=
field
.
$input
[
0
].
defaultValue
;
}
}
}
);
// Set a short-lived (5m / see postEdit.js) localStorage item to indicate which section is being edited.
if
(
section
)
{
mw
.
storage
.
session
.
set
(
pageName
+
'-editRecoverySection'
,
section
,
300
);
}
// Open indexedDB database and load any saved data that might be there.
storage
.
openDatabase
().
then
(
()
=>
{
// Check for and delete any expired data for any page, before loading any saved data for the current page.
storage
.
deleteExpiredData
().
then
(
()
=>
{
storage
.
loadData
(
pageName
,
section
).
then
(
onLoadData
);
}
);
}
);
// Set up cancel handler to delete data.
const
cancelButton
=
OO
.
ui
.
infuse
(
$editForm
.
find
(
'#mw-editform-cancel'
)[
0
]
);
cancelButton
.
on
(
'click'
,
()
=>
{
windowManager
.
openWindow
(
'abandonedit'
).
closed
.
then
(
(
data
)
=>
{
if
(
data
&&
data
.
action
===
'discard'
)
{
// Note that originalData is used below in onLoadData() but that's always called before this method.
// Here we set originalData to null in order to signal to saveFormData() to deleted the stored data.
originalData
=
null
;
storage
.
deleteData
(
pageName
,
section
).
finally
(
()
=>
{
mw
.
storage
.
session
.
remove
(
pageName
+
'-editRecoverySection'
);
// Release the beforeunload handler from mediawiki.action.edit.editWarning,
// per the documentation there
$
(
window
).
off
(
'beforeunload.editwarning'
);
location
.
href
=
cancelButton
.
getHref
();
}
);
}
}
);
}
);
}
function
track
(
metric
,
value
)
{
const
dbName
=
mw
.
config
.
get
(
'wgDBname'
);
mw
.
track
(
`counter.MediaWiki.edit_recovery.
${
metric
}
.by_wiki.
${
dbName
}
`
,
value
);
}
function
onLoadData
(
pageData
)
{
if
(
wasPosted
)
{
// If this is a POST request, save the current data (e.g. from a preview).
saveFormData
();
}
// If there is data stored, load it into the form.
if
(
!
wasPosted
&&
pageData
!==
undefined
&&
!
isSameAsOriginal
(
pageData
,
true
)
)
{
const
loadNotification
=
new
LoadNotification
(
{
differentRev
:
originalData
.
field_parentRevId
!==
pageData
.
field_parentRevId
}
);
// statsv: Track the number of times the edit recovery notification is shown.
track
(
'show'
,
1
);
const
notification
=
loadNotification
.
getNotification
();
// On 'restore changes'.
loadNotification
.
getRecoverButton
().
on
(
'click'
,
()
=>
{
loadData
(
pageData
);
notification
.
close
();
// statsv: Track the number of times the edit recovery data is recovered.
track
(
'recover'
,
1
);
}
);
// On 'discard changes'.
loadNotification
.
getDiscardButton
().
on
(
'click'
,
()
=>
{
loadData
(
originalData
);
storage
.
deleteData
(
pageName
,
section
).
then
(
()
=>
{
notification
.
close
();
}
);
// statsv: Track the number of times the edit recovery data is discarded.
track
(
'discard'
,
1
);
}
);
}
// Add change handlers.
Object
.
keys
(
inputFields
).
forEach
(
(
fieldName
)
=>
{
const
field
=
inputFields
[
fieldName
];
if
(
field
.
nodeName
!==
undefined
&&
field
.
nodeName
===
'TEXTAREA'
)
{
field
.
addEventListener
(
'input'
,
fieldChangeHandler
);
}
else
if
(
field
instanceof
OO
.
ui
.
Widget
)
{
field
.
on
(
'change'
,
fieldChangeHandler
);
}
else
{
field
.
addEventListener
(
'change'
,
fieldChangeHandler
);
}
}
);
// Also add handlers for when the window is closed or hidden. Saving the data at these points is not guaranteed to
// work, but it often does and the save operation is atomic so there's no harm in trying.
window
.
addEventListener
(
'beforeunload'
,
saveFormData
);
window
.
addEventListener
(
'blur'
,
saveFormData
);
/**
* Fired after EditRecovery has loaded any recovery data, added event handlers, etc.
*
* @event ~'editRecovery.loadEnd'
* @memberof Hooks
* @param {Object} editRecovery
* @param {Function} editRecovery.fieldChangeHandler
*/
mw
.
hook
(
'editRecovery.loadEnd'
).
fire
(
{
fieldChangeHandler
:
fieldChangeHandler
}
);
}
function
loadData
(
pageData
)
{
Object
.
keys
(
inputFields
).
forEach
(
(
fieldName
)
=>
{
if
(
pageData
[
fieldNamePrefix
+
fieldName
]
===
undefined
)
{
return
;
}
const
field
=
inputFields
[
fieldName
];
const
$field
=
$
(
field
);
// Set the field value depending on what type of field it is.
if
(
field
instanceof
OO
.
ui
.
CheckboxInputWidget
)
{
// OOUI checkbox widgets.
field
.
setSelected
(
pageData
[
fieldNamePrefix
+
fieldName
]
);
}
else
if
(
field
instanceof
OO
.
ui
.
Widget
)
{
// Other OOUI widgets.
field
.
setValue
(
pageData
[
fieldNamePrefix
+
fieldName
],
field
);
}
else
if
(
field
.
nodeName
===
'TEXTAREA'
)
{
// Textareas (also reset caret location to top).
$field
.
textSelection
(
'setContents'
,
pageData
[
fieldNamePrefix
+
fieldName
]
);
$field
.
textSelection
(
'setSelection'
,
{
start
:
0
}
);
}
else
{
// Anything else.
field
.
value
=
pageData
[
fieldNamePrefix
+
fieldName
];
}
}
);
}
function
fieldChangeHandler
()
{
clearTimeout
(
changeDebounceTimer
);
changeDebounceTimer
=
setTimeout
(
saveFormData
,
debounceTime
);
}
/**
* Compare a set of form field values to their original values (as at page load time).
*
* @ignore
* @param {Object} pageData The page data to compare to the original.
* @param {boolean} [ignoreRevIds=false] Do not use parent revision info when determining similarity.
* @return {boolean}
*/
function
isSameAsOriginal
(
pageData
,
ignoreRevIds
=
false
)
{
for
(
const
fieldName
in
inputFields
)
{
if
(
ignoreRevIds
&&
(
fieldName
===
'editRevId'
||
fieldName
===
'parentRevId'
)
)
{
continue
;
}
// Trim trailing whitespace from string fields, to approximate what PHP does when saving.
let
currentVal
=
pageData
[
fieldNamePrefix
+
fieldName
];
if
(
typeof
currentVal
===
'string'
)
{
currentVal
=
currentVal
.
replace
(
/\s+$/
,
''
);
}
let
originalVal
=
originalData
[
fieldNamePrefix
+
fieldName
];
if
(
typeof
originalVal
===
'string'
)
{
originalVal
=
originalVal
.
replace
(
/\s+$/
,
''
);
}
if
(
currentVal
!==
originalVal
)
{
return
false
;
}
}
return
true
;
}
function
saveFormData
()
{
const
pageData
=
getFormData
();
if
(
(
originalData
===
null
||
isSameAsOriginal
(
pageData
)
)
&&
!
wasPosted
)
{
// Delete the stored data if there's no change,
// or if we've flagged originalData as irrelevant,
// or if we can't determine this because this page was POSTed.
storage
.
deleteData
(
pageName
,
section
);
mw
.
storage
.
session
.
remove
(
'EditRecovery-data-saved'
);
}
else
{
storage
.
saveData
(
pageName
,
section
,
pageData
);
// Flag the data for deletion in the postEdit handler in ./postEdit.js
mw
.
storage
.
session
.
set
(
'EditRecovery-data-saved'
,
true
,
300
);
}
}
/**
* Get the current form data.
*
* @ignore
* @return {Object}
*/
function
getFormData
()
{
const
formData
=
{};
Object
.
keys
(
inputFields
).
forEach
(
(
fieldName
)
=>
{
const
field
=
inputFields
[
fieldName
];
let
newValue
=
null
;
if
(
!
(
field
instanceof
OO
.
ui
.
Widget
)
&&
field
.
nodeName
!==
undefined
&&
field
.
nodeName
===
'TEXTAREA'
)
{
// Text areas.
newValue
=
$
(
field
).
textSelection
(
'getContents'
);
}
else
if
(
field
instanceof
OO
.
ui
.
CheckboxInputWidget
)
{
// OOUI checkbox widgets.
newValue
=
field
.
isSelected
();
}
else
if
(
field
instanceof
OO
.
ui
.
Widget
)
{
// Other OOUI widgets.
newValue
=
field
.
getValue
();
}
else
{
// Anything else.
newValue
=
field
.
value
;
}
formData
[
fieldNamePrefix
+
fieldName
]
=
newValue
;
}
);
return
formData
;
}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sat, Oct 3, 09:34 (1 w, 3 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
1e/1d/fce4ea468368f2d5322692169eb7
Default Alt Text
edit.js (10 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment