Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F4127305
SourceEditorOverlay.js
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
18 KB
Referenced Files
None
Subscribers
None
SourceEditorOverlay.js
View Options
const
EditorOverlayBase
=
require
(
'./EditorOverlayBase'
),
util
=
require
(
'../mobile.startup/util'
),
icons
=
require
(
'../mobile.startup/icons'
),
Section
=
require
(
'../mobile.startup/Section'
),
saveFailureMessage
=
require
(
'./saveFailureMessage'
),
EditorGateway
=
require
(
'./EditorGateway'
),
fakeToolbar
=
require
(
'../mobile.init/fakeToolbar'
),
MessageBox
=
require
(
'../mobile.startup/MessageBox'
),
mfExtend
=
require
(
'../mobile.startup/mfExtend'
),
setPreferredEditor
=
require
(
'./setPreferredEditor'
),
VisualEditorOverlay
=
require
(
'./VisualEditorOverlay'
),
currentPage
=
require
(
'../mobile.startup/currentPage'
);
/**
* Overlay that shows an editor
*
* @class SourceEditorOverlay
* @uses Section
* @uses EditorGateway
* @uses VisualEditorOverlay
* @extends EditorOverlayBase
* @private
*
* @param {Object} options Configuration options
* @param {jQuery.Promise} [dataPromise] Optional promise for loading content
*/
function
SourceEditorOverlay
(
options
,
dataPromise
)
{
this
.
isFirefox
=
/firefox/i
.
test
(
window
.
navigator
.
userAgent
);
this
.
gateway
=
new
EditorGateway
(
{
api
:
options
.
api
,
title
:
options
.
title
,
sectionId
:
options
.
sectionId
,
oldId
:
options
.
oldId
,
fromModified
:
!!
dataPromise
,
preload
:
options
.
preload
,
preloadparams
:
options
.
preloadparams
,
editintro
:
options
.
editintro
}
);
this
.
readOnly
=
!!
options
.
oldId
;
// If old revision, readOnly mode
this
.
dataPromise
=
dataPromise
||
this
.
gateway
.
getContent
();
this
.
currentPage
=
currentPage
();
if
(
this
.
currentPage
.
isVEVisualAvailable
()
)
{
options
.
editSwitcher
=
true
;
}
if
(
this
.
readOnly
)
{
options
.
readOnly
=
true
;
options
.
editingMsg
=
mw
.
msg
(
'mobile-frontend-editor-viewing-source-page'
,
options
.
title
);
}
else
{
options
.
editingMsg
=
mw
.
msg
(
'mobile-frontend-editor-editing-page'
,
options
.
title
);
}
options
.
previewingMsg
=
mw
.
msg
(
'mobile-frontend-editor-previewing-page'
,
options
.
title
);
EditorOverlayBase
.
call
(
this
,
util
.
extend
(
true
,
{
events
:
{
'input .wikitext-editor'
:
'onInputWikitextEditor'
}
},
options
)
);
}
mfExtend
(
SourceEditorOverlay
,
EditorOverlayBase
,
{
/**
* @inheritdoc
* @memberof SourceEditorOverlay
* @instance
*/
templatePartials
:
util
.
extend
(
{},
EditorOverlayBase
.
prototype
.
templatePartials
,
{
content
:
util
.
template
(
`
<div lang="{{contentLang}}" dir="{{contentDir}}" class="editor-container content">
<textarea class="wikitext-editor" id="wikitext-editor" cols="40" rows="10" placeholder="{{placeholder}}"></textarea>
<div class="preview collapsible-headings-expanded mw-body-content"></div>
</div>
`
)
}
),
/**
* @memberof SourceEditorOverlay
* @instance
*/
editor
:
'wikitext'
,
/**
* @memberof SourceEditorOverlay
* @instance
*/
sectionLine
:
''
,
/**
* @inheritdoc
* @memberof SourceEditorOverlay
* @instance
*/
show
:
function
()
{
EditorOverlayBase
.
prototype
.
show
.
apply
(
this
,
arguments
);
// Ensure we do this after showing the overlay, otherwise it doesn't work.
this
.
_resizeEditor
();
},
/**
* Wikitext Editor input handler
*
* @memberof SourceEditorOverlay
* @instance
*/
onInputWikitextEditor
:
function
()
{
this
.
gateway
.
setContent
(
this
.
$el
.
find
(
'.wikitext-editor'
).
val
()
);
this
.
$el
.
find
(
'.continue, .submit'
).
prop
(
'disabled'
,
false
);
},
/**
* @inheritdoc
* @memberof SourceEditorOverlay
* @instance
*/
onClickBack
:
function
()
{
EditorOverlayBase
.
prototype
.
onClickBack
.
apply
(
this
,
arguments
);
this
.
_hidePreview
();
},
/**
* @inheritdoc
* @memberof SourceEditorOverlay
* @instance
*/
postRender
:
function
()
{
const
self
=
this
;
// log edit attempt
this
.
log
(
{
action
:
'ready'
}
);
this
.
log
(
{
action
:
'loaded'
}
);
if
(
this
.
currentPage
.
isVEVisualAvailable
()
)
{
mw
.
loader
.
using
(
'ext.visualEditor.switching'
).
then
(
function
()
{
const
toolFactory
=
new
OO
.
ui
.
ToolFactory
(),
toolGroupFactory
=
new
OO
.
ui
.
ToolGroupFactory
();
toolFactory
.
register
(
mw
.
libs
.
ve
.
MWEditModeVisualTool
);
toolFactory
.
register
(
mw
.
libs
.
ve
.
MWEditModeSourceTool
);
const
switchToolbar
=
new
OO
.
ui
.
Toolbar
(
toolFactory
,
toolGroupFactory
,
{
classes
:
[
'editor-switcher'
]
}
);
switchToolbar
.
on
(
'switchEditor'
,
function
(
mode
)
{
if
(
mode
===
'visual'
)
{
if
(
!
self
.
gateway
.
hasChanged
)
{
self
.
_switchToVisualEditor
();
}
else
{
// Pass wikitext if there are changes.
self
.
_switchToVisualEditor
(
self
.
gateway
.
content
);
}
}
}
);
switchToolbar
.
setup
(
[
{
name
:
'editMode'
,
type
:
'list'
,
icon
:
'edit'
,
title
:
mw
.
msg
(
'visualeditor-mweditmode-tooltip'
),
include
:
[
'editModeVisual'
,
'editModeSource'
]
}
]
);
self
.
$el
.
find
(
'.switcher-container'
).
html
(
switchToolbar
.
$element
);
switchToolbar
.
emit
(
'updateState'
);
}
);
}
EditorOverlayBase
.
prototype
.
postRender
.
apply
(
this
);
// This spinner is still used when displaying save/preview panel
this
.
$el
.
find
(
'.overlay-content'
).
append
(
icons
.
spinner
().
$el
);
this
.
hideSpinner
();
this
.
$preview
=
this
.
$el
.
find
(
'.preview'
);
this
.
$content
=
this
.
$el
.
find
(
'.wikitext-editor'
);
// The following classes can be used here:
// * mw-editfont-monospace
// * mw-editfont-sans-serif
// * mw-editfont-serif
this
.
$content
.
addClass
(
'mw-editfont-'
+
mw
.
user
.
options
.
get
(
'editfont'
)
);
// make license links open in separate tabs
this
.
$el
.
find
(
'.license a'
).
attr
(
'target'
,
'_blank'
);
// If in readOnly mode, make textarea readonly
if
(
this
.
readOnly
)
{
this
.
$content
.
prop
(
'readonly'
,
true
);
}
this
.
$content
.
on
(
'input'
,
this
.
_resizeEditor
.
bind
(
this
)
)
.
one
(
'input'
,
function
()
{
self
.
log
(
{
action
:
'firstChange'
}
);
}
);
if
(
this
.
isFirefox
)
{
this
.
$content
.
on
(
'mousedown'
,
function
()
{
// Support: Firefox Mobile
// Firefox scrolls back to the top of the page *every time*
// you tap on the textarea. This makes things slightly
// more usable by restoring your scroll offset every time
// the page scrolls for the next 1000ms.
// The page will still flicker every time the user touches
// to place the cursor, but this is better than completely
// losing your scroll offset. (T214880)
const
docEl
=
document
.
documentElement
,
scrollTop
=
docEl
.
scrollTop
;
function
blockScroll
()
{
docEl
.
scrollTop
=
scrollTop
;
}
window
.
addEventListener
(
'scroll'
,
blockScroll
);
setTimeout
(
function
()
{
window
.
removeEventListener
(
'scroll'
,
blockScroll
);
},
1000
);
}
);
}
// Render edit summary
this
.
summaryTextArea
=
new
OO
.
ui
.
MultilineTextInputWidget
(
{
placeholder
:
this
.
options
.
summaryMsg
,
classes
:
[
'summary'
],
value
:
''
,
maxRows
:
2
}
);
this
.
$el
.
find
(
'.summary-input'
).
append
(
this
.
summaryTextArea
.
$element
);
this
.
_loadContent
();
},
/**
* Handles click on "Edit without login" in anonymous editing warning.
*
* @memberof SourceEditorOverlay
* @instance
* @private
*/
onClickAnonymous
:
function
()
{
this
.
$anonWarning
.
hide
();
this
.
$anonTalkWarning
.
hide
();
// reenable "Next" button
this
.
$anonHiddenButtons
.
show
();
this
.
$content
.
show
();
this
.
_resizeEditor
();
},
/**
* Prepares the preview interface and reveals the save screen of the overlay
*
* @inheritdoc
* @memberof SourceEditorOverlay
* @instance
*/
onStageChanges
:
function
()
{
const
self
=
this
,
params
=
{
text
:
this
.
getContent
()
};
this
.
scrollTop
=
util
.
getDocument
().
find
(
'body'
).
scrollTop
();
this
.
$content
.
hide
();
this
.
showSpinner
();
if
(
mw
.
config
.
get
(
'wgIsMainPage'
)
)
{
params
.
mainpage
=
1
;
// Setting it to 0 will have the same effect
}
function
hideSpinnerAndShowPreview
()
{
self
.
hideSpinner
();
self
.
$preview
.
show
();
mw
.
hook
(
'wikipage.content'
).
fire
(
self
.
$preview
);
}
this
.
gateway
.
getPreview
(
params
).
then
(
function
(
result
)
{
const
parsedText
=
result
.
text
,
parsedSectionLine
=
result
.
line
;
self
.
sectionId
=
result
.
id
;
// On desktop edit summaries strip tags. Mimic this behavior on mobile devices
self
.
sectionLine
=
self
.
parseHTML
(
'<div>'
).
html
(
parsedSectionLine
).
text
();
new
Section
(
{
el
:
self
.
$preview
,
text
:
parsedText
}
).
$el
.
find
(
'a'
).
on
(
'click'
,
false
);
hideSpinnerAndShowPreview
();
},
function
()
{
self
.
$preview
.
replaceWith
(
new
MessageBox
(
{
type
:
'error'
,
msg
:
mw
.
msg
(
'mobile-frontend-editor-error-preview'
)
}
).
$el
);
hideSpinnerAndShowPreview
();
}
);
EditorOverlayBase
.
prototype
.
onStageChanges
.
apply
(
this
,
arguments
);
},
/**
* Hides the preview and reverts back to initial screen.
*
* @memberof SourceEditorOverlay
* @instance
* @private
*/
_hidePreview
:
function
()
{
this
.
gateway
.
abortPreview
();
this
.
hideSpinner
();
// FIXME: Don't rely on internals - we re-render template instead.
this
.
$preview
.
removeClass
(
'cdx-message--error'
).
hide
();
this
.
$content
.
show
();
window
.
scrollTo
(
0
,
this
.
scrollTop
);
this
.
showHidden
(
'.initial-header'
);
},
/**
* Resize the editor textarea, maintaining scroll position in iOS
*
* @memberof SourceEditorOverlay
* @instance
*/
_resizeEditor
:
function
()
{
let
scrollTop
,
container
,
$scrollContainer
;
if
(
!
this
.
$scrollContainer
)
{
container
=
OO
.
ui
.
Element
.
static
.
getClosestScrollableContainer
(
this
.
$content
[
0
]
);
// The scroll container will be either within the view
// or the document element itself.
$scrollContainer
=
this
.
$el
.
find
(
container
).
length
?
this
.
$el
.
find
(
container
)
:
util
.
getDocument
();
this
.
$scrollContainer
=
$scrollContainer
;
this
.
$content
.
css
(
'padding-bottom'
,
this
.
$scrollContainer
.
height
()
*
0.6
);
}
else
{
$scrollContainer
=
this
.
$scrollContainer
;
}
// Only do this if scroll container exists
if
(
this
.
$content
.
prop
(
'scrollHeight'
)
&&
$scrollContainer
.
length
)
{
scrollTop
=
$scrollContainer
.
scrollTop
();
this
.
$content
.
css
(
'height'
,
'auto'
)
// can't reuse prop( 'scrollHeight' ) because we need the current value
.
css
(
'height'
,
(
this
.
$content
.
prop
(
'scrollHeight'
)
+
2
)
+
'px'
);
$scrollContainer
.
scrollTop
(
scrollTop
);
}
},
/**
* Set content to the user input field.
*
* @memberof SourceEditorOverlay
* @instance
* @param {string} content The content to set.
*/
setContent
:
function
(
content
)
{
this
.
$content
.
show
()
.
val
(
content
);
this
.
_resizeEditor
();
},
/**
* Returns the content of the user input field.
*
* @memberof SourceEditorOverlay
* @instance
* @return {string}
*/
getContent
:
function
()
{
return
this
.
$content
.
val
();
},
/**
* Requests content from the API and reveals it in UI.
*
* @memberof SourceEditorOverlay
* @instance
* @private
*/
_loadContent
:
function
()
{
const
self
=
this
;
this
.
$content
.
hide
();
this
.
getLoadingPromise
()
.
then
(
function
(
result
)
{
const
content
=
result
.
text
;
self
.
setContent
(
content
);
// If the loaded content is not the default content, enable the save button
if
(
self
.
hasChanged
()
)
{
self
.
$el
.
find
(
'.continue, .submit'
).
prop
(
'disabled'
,
false
);
}
const
options
=
self
.
options
;
const
showAnonWarning
=
options
.
isAnon
&&
!
options
.
switched
;
if
(
showAnonWarning
)
{
self
.
$anonWarning
=
self
.
createAnonWarning
(
options
);
self
.
$anonTalkWarning
=
self
.
createAnonTalkWarning
();
self
.
$el
.
find
(
'.editor-container'
).
append
(
[
self
.
$anonTalkWarning
,
self
.
$anonWarning
]
);
self
.
$content
.
hide
();
// the user has to click login, signup or edit without login,
// disable "Next" button on top right
self
.
$anonHiddenButtons
=
self
.
$el
.
find
(
'.overlay-header .continue'
).
hide
();
}
if
(
self
.
gateway
.
fromModified
)
{
// Trigger intial EditorGateway#setContent and update save button
self
.
onInputWikitextEditor
();
}
self
.
showEditNotices
();
}
);
},
/**
* Loads a {VisualEditorOverlay} and replaces the existing SourceEditorOverlay with it
* based on the current option values.
*
* @memberof SourceEditorOverlay
* @instance
* @private
* @param {string} [wikitext] Wikitext to pass to VE
*/
_switchToVisualEditor
:
function
(
wikitext
)
{
const
self
=
this
;
this
.
log
(
{
action
:
'abort'
,
type
:
'switchnochange'
,
mechanism
:
'navigate'
}
);
this
.
logFeatureUse
(
{
feature
:
'editor-switch'
,
action
:
'visual-mobile'
}
);
// Save a user setting indicating that this user prefers using the VisualEditor
setPreferredEditor
(
'VisualEditor'
);
this
.
$el
.
addClass
(
'switching'
);
this
.
$el
.
find
(
'.overlay-header-container'
).
hide
();
this
.
$el
.
append
(
fakeToolbar
()
);
this
.
$content
.
prop
(
'readonly'
,
true
);
mw
.
loader
.
using
(
'ext.visualEditor.targetLoader'
).
then
(
function
()
{
mw
.
libs
.
ve
.
targetLoader
.
addPlugin
(
'ext.visualEditor.mobileArticleTarget'
);
return
mw
.
libs
.
ve
.
targetLoader
.
loadModules
(
'visual'
);
}
).
then
(
function
()
{
const
options
=
self
.
getOptionsForSwitch
();
options
.
SourceEditorOverlay
=
SourceEditorOverlay
;
if
(
wikitext
)
{
options
.
dataPromise
=
mw
.
libs
.
ve
.
targetLoader
.
requestPageData
(
'visual'
,
mw
.
config
.
get
(
'wgRelevantPageName'
),
{
section
:
options
.
sectionId
,
oldId
:
options
.
oldId
||
mw
.
config
.
get
(
'wgRevisionId'
),
targetName
:
'mobile'
,
modified
:
true
,
wikitext
:
wikitext
}
);
}
else
{
delete
options
.
dataPromise
;
}
const
newOverlay
=
new
VisualEditorOverlay
(
options
);
newOverlay
.
getLoadingPromise
().
then
(
function
()
{
self
.
switching
=
true
;
self
.
overlayManager
.
replaceCurrent
(
newOverlay
);
self
.
switching
=
false
;
}
);
},
function
()
{
self
.
$el
.
removeClass
(
'switching'
);
self
.
$el
.
find
(
'.overlay-header-container'
).
show
();
self
.
$el
.
find
(
'.ve-mobile-fakeToolbar-container'
).
remove
();
self
.
$content
.
prop
(
'readonly'
,
false
);
// FIXME: We should show an error notification, but right now toast
// notifications are not dismissible when shown within the editor.
}
);
},
/**
* Get the current edit summary.
*
* @memberof SourceEditorOverlay
* @return {string}
*/
getEditSummary
:
function
()
{
return
this
.
summaryTextArea
.
getValue
();
},
/**
* Executed when the editor clicks the save/publish button. Handles logging and submitting
* the save action to the editor API.
*
* @inheritdoc
* @memberof SourceEditorOverlay
* @instance
*/
onSaveBegin
:
function
()
{
const
self
=
this
,
options
=
{
summary
:
this
.
getEditSummary
()
};
if
(
self
.
sectionLine
!==
''
)
{
options
.
summary
=
'/* '
+
self
.
sectionLine
+
' */'
+
options
.
summary
;
}
EditorOverlayBase
.
prototype
.
onSaveBegin
.
apply
(
this
,
arguments
);
if
(
this
.
confirmAborted
)
{
return
;
}
if
(
this
.
captchaId
)
{
options
.
captchaId
=
this
.
captchaId
;
options
.
captchaWord
=
this
.
$el
.
find
(
'.captcha-word'
).
val
();
}
this
.
showHidden
(
'.saving-header'
);
this
.
gateway
.
save
(
options
)
.
then
(
function
(
newRevId
,
redirectUrl
,
tempUserCreated
)
{
const
title
=
self
.
options
.
title
;
// Special case behaviour of main page
if
(
mw
.
config
.
get
(
'wgIsMainPage'
)
&&
!
redirectUrl
)
{
// FIXME: Blocked on T189173
// eslint-disable-next-line no-restricted-properties
window
.
location
=
mw
.
util
.
getUrl
(
title
);
return
;
}
self
.
onSaveComplete
(
newRevId
,
redirectUrl
,
tempUserCreated
);
if
(
redirectUrl
&&
tempUserCreated
)
{
// eslint-disable-next-line no-restricted-properties
window
.
location
.
href
=
redirectUrl
;
}
},
function
(
data
)
{
self
.
onSaveFailure
(
data
);
}
);
},
/**
* @inheritdoc
* @memberof SourceEditorOverlay
* @instance
* @param {number|null} newRevId ID of the newly created revision, or null if it was a
* null edit.
* @param {string} [redirectUrl] URL to redirect to, if different than the current URL.
* @param {boolean} [tempUserCreated] Whether a temporary user was created
*/
onSaveComplete
:
function
(
newRevId
,
redirectUrl
)
{
EditorOverlayBase
.
prototype
.
onSaveComplete
.
apply
(
this
,
arguments
);
// The parent class changes the location hash in a setTimeout, so wait
// for that to happen before reloading.
setTimeout
(
function
()
{
if
(
redirectUrl
)
{
// eslint-disable-next-line no-restricted-properties
window
.
location
.
href
=
redirectUrl
;
}
else
if
(
newRevId
)
{
// Set a notify parameter similar to venotify in VisualEditor.
const
url
=
new
URL
(
location
.
href
);
url
.
searchParams
.
set
(
'mfnotify'
,
this
.
isNewPage
?
'created'
:
'saved'
);
// eslint-disable-next-line no-restricted-properties
window
.
location
.
search
=
url
.
search
;
}
else
{
// Null edit; do not add notify parameter.
// Note the "#" may be in the URL.
// If so, using window.location alone will not reload the page
// we need to forcefully refresh
// eslint-disable-next-line no-restricted-properties
window
.
location
.
reload
();
}
}
);
},
/**
* @inheritdoc
* @memberof SourceEditorOverlay
* @instance
*/
showSaveCompleteMsg
:
function
(
action
,
tempUserCreated
)
{
__non_webpack_require__
(
'mediawiki.action.view.postEdit'
).
fireHookOnPageReload
(
action
,
tempUserCreated
);
},
/**
* Executed when page save fails. Handles error display and bookkeeping,
* passes logging duties to the parent.
*
* @inheritdoc
* @memberof SourceEditorOverlay
* @instance
*/
onSaveFailure
:
function
(
data
)
{
let
msg
,
noRetry
;
if
(
data
.
edit
&&
data
.
edit
.
captcha
)
{
this
.
captchaId
=
data
.
edit
.
captcha
.
id
;
this
.
handleCaptcha
(
data
.
edit
.
captcha
);
}
else
{
msg
=
saveFailureMessage
(
data
);
this
.
reportError
(
msg
);
this
.
showHidden
(
'.save-header, .save-panel'
);
// Some errors may be temporary, but for others we know for sure that the save will
// never succeed, so don't confuse the user by giving them the option to retry.
noRetry
=
data
.
errors
&&
data
.
errors
.
some
(
function
(
error
)
{
return
error
.
code
===
'abusefilter-disallowed'
;
}
);
if
(
noRetry
)
{
// disable continue and save buttons, reenabled when user changes content
this
.
$el
.
find
(
'.continue, .submit'
).
prop
(
'disabled'
,
true
);
}
}
EditorOverlayBase
.
prototype
.
onSaveFailure
.
apply
(
this
,
arguments
);
},
/**
* Checks whether the existing content has changed.
*
* @memberof SourceEditorOverlay
* @instance
* @return {boolean}
*/
hasChanged
:
function
()
{
return
this
.
gateway
.
hasChanged
;
}
}
);
module
.
exports
=
SourceEditorOverlay
;
File Metadata
Details
Attached
Mime Type
text/csv
Expires
Wed, Aug 19, 08:31 (2 w, 3 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
f7/e5/a258764b52b0d6ca73c16c194403
Default Alt Text
SourceEditorOverlay.js (18 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment