Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F4142082
RealtimePreview.js
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
13 KB
Referenced Files
None
Subscribers
None
RealtimePreview.js
View Options
const
ResizingDragBar
=
require
(
'./ResizingDragBar.js'
);
const
TwoPaneLayout
=
require
(
'./TwoPaneLayout.js'
);
const
ErrorLayout
=
require
(
'./ErrorLayout.js'
);
const
ManualWidget
=
require
(
'./ManualWidget.js'
);
const
localStorage
=
require
(
'mediawiki.storage'
).
local
;
/**
* @class
*/
function
RealtimePreview
()
{
this
.
configData
=
mw
.
loader
.
moduleRegistry
[
'ext.wikiEditor'
].
script
.
files
[
'data.json'
];
// Preference name, must match what's in extension.json and Hooks.php.
this
.
prefName
=
'wikieditor-realtimepreview'
;
this
.
userPref
=
this
.
getUserPref
();
this
.
enabled
=
this
.
userPref
;
this
.
twoPaneLayout
=
new
TwoPaneLayout
();
this
.
pagePreview
=
require
(
'mediawiki.page.preview'
);
// @todo This shouldn't be required, but the preview element is added in PHP
// and can have attributes with values (such as `dir`) that aren't easily accessible from here,
// and we need to duplicate here what Live Preview does in core.
const
$previewContent
=
$
(
'#wikiPreview .mw-content-ltr, #wikiPreview .mw-content-rtl'
).
first
().
clone
();
this
.
$previewNode
=
$
(
'<div>'
)
.
addClass
(
'ext-WikiEditor-realtimepreview-preview'
)
.
attr
(
'tabindex'
,
'1'
)
// T317108
.
append
(
$previewContent
);
// Loading bar.
this
.
$loadingBar
=
$
(
'<div>'
).
addClass
(
'ext-WikiEditor-realtimepreview-loadingbar'
).
append
(
'<div>'
);
this
.
$loadingBar
.
hide
();
// Error layout.
this
.
errorLayout
=
new
ErrorLayout
();
this
.
errorLayout
.
getReloadButton
().
connect
(
this
,
{
click
:
function
()
{
this
.
doRealtimePreview
(
true
);
mw
.
hook
(
'ext.WikiEditor.realtimepreview.reloadError'
).
fire
(
this
);
}.
bind
(
this
)
}
);
// Manual reload button (visible on hover).
this
.
reloadButton
=
new
OO
.
ui
.
ButtonWidget
(
{
classes
:
[
'ext-WikiEditor-reloadButton'
],
icon
:
'reload'
,
label
:
mw
.
msg
(
'wikieditor-realtimepreview-reload'
),
accessKey
:
mw
.
msg
(
'accesskey-wikieditor-realtimepreview'
),
title
:
mw
.
msg
(
'wikieditor-realtimepreview-reload-title'
)
}
);
this
.
reloadButton
.
connect
(
this
,
{
click
:
function
()
{
// Only refresh the preview if we're enabled.
if
(
this
.
enabled
)
{
this
.
doRealtimePreview
(
true
);
}
// Let other things happen after refreshing.
mw
.
hook
(
'ext.WikiEditor.realtimepreview.reloadHover'
).
fire
(
this
);
}.
bind
(
this
)
}
);
// Manual mode widget.
this
.
manualWidget
=
new
ManualWidget
(
this
,
this
.
reloadButton
);
// Set up a property for reducedMotion — useful for customising the UI message.
this
.
reducedMotion
=
window
.
matchMedia
(
'(prefers-reduced-motion: reduce)'
).
matches
;
// If the user has "prefers-reduced-motion" set, force us into manual mode.
this
.
inManualMode
=
this
.
reducedMotion
;
this
.
twoPaneLayout
.
getPane2
().
append
(
this
.
manualWidget
.
$element
,
this
.
reloadButton
.
$element
,
this
.
$loadingBar
,
this
.
$previewNode
,
this
.
errorLayout
.
$element
);
this
.
eventNames
=
'change.realtimepreview input.realtimepreview cut.realtimepreview paste.realtimepreview'
;
// Used to ensure we wait for a response before making new requests.
this
.
isPreviewing
=
false
;
this
.
previewPending
=
false
;
this
.
lastWikitext
=
null
;
// Used to average response times and automatically disable realtime preview if it's very slow.
this
.
responseTimes
=
[];
// Used to hide the 'abort' error when submitting the #editform.
this
.
isSubmitting
=
false
;
}
/**
* @public
* @param {Object} context The WikiEditor context.
* @return {jQuery}
*/
RealtimePreview
.
prototype
.
getToolbarButton
=
function
(
context
)
{
this
.
context
=
context
;
const
$uiText
=
context
.
$ui
.
find
(
'.wikiEditor-ui-text'
);
// Fix the height of the textarea, before adding a resizing bar below it.
const
height
=
context
.
$textarea
.
height
();
$uiText
.
css
(
'height'
,
height
+
'px'
);
context
.
$textarea
.
removeAttr
(
'rows cols'
);
context
.
$textarea
.
addClass
(
'ext-WikiEditor-realtimepreview-textbox'
);
// Add the resizing bar.
const
bottomDragBar
=
new
ResizingDragBar
(
{
isEW
:
false
,
id
:
'ext-WikiEditor-bottom-dragbar'
}
);
$uiText
.
after
(
bottomDragBar
.
$element
);
// Create and configure the toolbar button.
this
.
button
=
new
OO
.
ui
.
ToggleButtonWidget
(
{
label
:
mw
.
msg
(
'wikieditor-realtimepreview-preview'
),
icon
:
'article'
,
value
:
this
.
enabled
,
framed
:
false
,
// T305953; So we can find usage of this class later: .tool
classes
:
[
'tool'
,
'ext-WikiEditor-realtimepreview-button'
]
}
);
this
.
button
.
connect
(
this
,
{
change
:
[
this
.
toggle
,
true
]
}
);
if
(
!
this
.
isScreenWideEnough
()
)
{
this
.
enabled
=
false
;
this
.
button
.
toggle
(
false
);
}
// Hide or show the preview and toolbar button when the window is resized.
$
(
window
).
on
(
'resize'
,
this
.
enableFeatureWhenScreenIsWideEnough
.
bind
(
this
)
);
// Remove the old onboarding-status storage that was discontinued in March 2023.
localStorage
.
remove
(
'WikiEditor-RealtimePreview-onboarding-dismissed'
);
return
$
(
'<div>'
).
append
(
this
.
button
.
$element
);
};
/**
* Get the user preference for Realtime Preview.
*
* @public
* @return {boolean}
*/
RealtimePreview
.
prototype
.
getUserPref
=
function
()
{
return
(
typeof
this
.
userPref
!==
'undefined'
)
?
this
.
userPref
:
mw
.
user
.
options
.
get
(
this
.
prefName
)
>
0
;
};
/**
* Enable or disable Realtime Preview.
*
* @param {boolean} [enable=true] Whether to enable or disable.
* @param {boolean} [saveUserPref=true] Whether to save the user preference.
* @public
*/
RealtimePreview
.
prototype
.
setEnabled
=
function
(
enable
,
saveUserPref
)
{
// Set this.enabled to the opposite of what we want, and then toggle it to the desired state.
this
.
enabled
=
(
typeof
enable
===
'boolean'
)
?
!
enable
:
false
;
this
.
toggle
(
saveUserPref
);
};
/**
* Save the user preference for Realtime Preview.
*
* @private
*/
RealtimePreview
.
prototype
.
saveUserPref
=
function
()
{
this
.
userPref
=
this
.
enabled
?
1
:
0
;
(
new
mw
.
Api
()
).
saveOption
(
this
.
prefName
,
this
.
userPref
);
};
/**
* Toggle the two-pane preview display.
*
* @param {boolean} [saveUserPref=true] Whether to save the user preference.
* @private
*/
RealtimePreview
.
prototype
.
toggle
=
function
(
saveUserPref
)
{
const
$uiText
=
this
.
context
.
$ui
.
find
(
'.wikiEditor-ui-text'
);
const
$textarea
=
this
.
context
.
$textarea
;
const
$form
=
$textarea
.
parents
(
'form'
);
// Save the current cursor selection and focused element.
const
cursorPos
=
$textarea
.
textSelection
(
'getCaretPosition'
,
{
startAndEnd
:
true
}
);
const
focusedElement
=
document
.
activeElement
;
// Remove or add the layout to the DOM.
if
(
this
.
enabled
)
{
// Move height from the TwoPaneLayout to the text UI div.
$uiText
.
css
(
'height'
,
this
.
twoPaneLayout
.
$element
.
height
()
+
'px'
);
// Put the text div back to being after the layout, and then hide the layout.
this
.
twoPaneLayout
.
$element
.
after
(
$uiText
);
this
.
twoPaneLayout
.
$element
.
hide
();
// Remove the keyup handler.
$textarea
.
off
(
this
.
eventNames
);
$form
.
off
(
'reset.realtimepreview'
);
// Remove the submit handler.
$form
.
off
(
'submit.realtimepreview'
);
this
.
isSubmitting
=
false
;
// Let other things happen after disabling.
mw
.
hook
(
'ext.WikiEditor.realtimepreview.disable'
).
fire
(
this
);
}
else
{
// Add the layout before the text div of the UI and then move the text div into it.
$uiText
.
before
(
this
.
twoPaneLayout
.
$element
);
this
.
twoPaneLayout
.
setPane1
(
$uiText
);
this
.
twoPaneLayout
.
$element
.
show
();
// Move explicit height from text-ui (which may have been set via manual resizing), to panes.
this
.
twoPaneLayout
.
$element
.
css
(
'height'
,
$uiText
.
height
()
+
'px'
);
$uiText
.
css
(
'height'
,
'100%'
);
// Load the preview when enabling,
this
.
doRealtimePreview
();
// and also on keyup, change, paste etc.
$textarea
.
off
(
this
.
eventNames
)
.
on
(
this
.
eventNames
,
this
.
getEventHandler
()
);
$form
.
off
(
'reset.realtimepreview'
)
.
on
(
'reset.realtimepreview'
,
this
.
getEventHandler
()
);
// Hide or show the manual-reload message bar.
this
.
manualWidget
.
toggle
(
this
.
inManualMode
);
$form
.
on
(
'submit.realtimepreview'
,
()
=>
{
this
.
isSubmitting
=
true
;
}
);
// Let other things happen after enabling.
mw
.
hook
(
'ext.WikiEditor.realtimepreview.enable'
).
fire
(
this
);
}
// Restore current selection.
$textarea
.
textSelection
(
'setSelection'
,
{
start
:
cursorPos
[
0
],
end
:
cursorPos
[
1
]
}
);
$textarea
.
textSelection
(
'scrollToCaretPosition'
);
// Focus on whatever had focus before, in case it wasn't the textarea.
focusedElement
.
focus
();
// Record the toggle state and update the button.
this
.
enabled
=
!
this
.
enabled
;
this
.
button
.
$element
.
toggleClass
(
'tool-active'
,
this
.
enabled
);
// T305953
this
.
button
.
setFlags
(
{
progressive
:
this
.
enabled
}
);
if
(
typeof
saveUserPref
===
'undefined'
||
(
typeof
saveUserPref
===
'boolean'
&&
saveUserPref
)
)
{
this
.
saveUserPref
();
}
};
/**
* @public
* @return {Function}
*/
RealtimePreview
.
prototype
.
getEventHandler
=
function
()
{
return
mw
.
util
.
debounce
(
()
=>
{
// Only do preview if we're not in manual mode (as set in this.checkResponseTimes()).
if
(
!
this
.
inManualMode
)
{
this
.
doRealtimePreview
();
}
},
this
.
configData
.
realtimeDebounce
);
};
/**
* Check if screen meets minimum width requirement for Realtime Preview.
*
* @public
* @return {boolean}
*/
RealtimePreview
.
prototype
.
isScreenWideEnough
=
function
()
{
return
this
.
context
.
$ui
.
width
()
>
600
;
};
/**
* Display feature (buttons and functionality) only when screen is wide enough
*
* @private
*/
RealtimePreview
.
prototype
.
enableFeatureWhenScreenIsWideEnough
=
function
()
{
const
previewButtonIsVisible
=
this
.
button
.
isVisible
();
const
isScreenWideEnough
=
this
.
isScreenWideEnough
();
if
(
!
isScreenWideEnough
&&
previewButtonIsVisible
)
{
this
.
button
.
toggle
(
false
);
this
.
reloadButton
.
setDisabled
(
true
);
if
(
this
.
enabled
)
{
this
.
setEnabled
(
false
,
false
);
}
}
else
if
(
isScreenWideEnough
&&
!
previewButtonIsVisible
)
{
this
.
button
.
toggle
(
true
);
this
.
reloadButton
.
setDisabled
(
false
);
// if user preference and realtime disable
if
(
!
this
.
enabled
&&
this
.
getUserPref
()
)
{
this
.
setEnabled
(
true
,
false
);
}
}
};
/**
* @private
* @param {jQuery} $msg
*/
RealtimePreview
.
prototype
.
showError
=
function
(
$msg
)
{
if
(
this
.
isSubmitting
)
{
// Do not show any error if the form has been submitted (the page is being reloaded and
// any preview request will have been aborted).
return
;
}
this
.
$previewNode
.
hide
();
this
.
reloadButton
.
toggle
(
false
);
this
.
manualWidget
.
toggle
(
false
);
// There is no need for a default message because mw.Api.getErrorMessage() will
// always provide something (even for no network connection, server-side fatal errors, etc.).
this
.
errorLayout
.
setMessage
(
$msg
);
this
.
errorLayout
.
toggle
(
true
);
};
/**
* @private
* @param {number} time
*/
RealtimePreview
.
prototype
.
checkResponseTimes
=
function
(
time
)
{
// Don't track response times if we're already in manual mode or an error is shown.
if
(
this
.
inManualMode
||
this
.
errorLayout
.
isVisible
()
)
{
return
;
}
this
.
responseTimes
.
push
(
Date
.
now
()
-
time
);
if
(
this
.
responseTimes
.
length
<
3
)
{
return
;
}
const
totalResponseTime
=
this
.
responseTimes
.
reduce
(
(
a
,
b
)
=>
a
+
b
,
0
);
if
(
(
totalResponseTime
/
this
.
responseTimes
.
length
)
>
this
.
configData
.
realtimeDisableDuration
)
{
this
.
inManualMode
=
true
;
this
.
manualWidget
.
toggle
(
true
);
mw
.
hook
(
'ext.WikiEditor.realtimepreview.stop'
).
fire
(
this
);
}
this
.
responseTimes
.
shift
();
};
/**
* @private
* @param {boolean} forceUpdate For the preview to update, even if the wikitext is unchanged,
* e.g. when the user presses the 'reload' button.
*/
RealtimePreview
.
prototype
.
doRealtimePreview
=
function
(
forceUpdate
)
{
// Wait for a response before making any new requests.
if
(
this
.
isPreviewing
)
{
// Queue up one final preview once this one finishes.
this
.
previewPending
=
true
;
return
;
}
const
$textareaNode
=
$
(
'#wpTextbox1'
);
const
wikitext
=
$textareaNode
.
textSelection
(
'getContents'
);
if
(
!
forceUpdate
&&
wikitext
===
this
.
lastWikitext
)
{
// Wikitext unchanged, no update necessary
return
;
}
this
.
lastWikitext
=
wikitext
;
this
.
isPreviewing
=
true
;
this
.
$loadingBar
.
show
();
this
.
$previewNode
.
show
();
this
.
reloadButton
.
setDisabled
(
true
);
this
.
manualWidget
.
setDisabled
(
true
);
this
.
errorLayout
.
toggle
(
false
);
const
loadingSelectors
=
this
.
pagePreview
.
getLoadingSelectors
()
// config.$previewNode below is a clone of #wikiPreview with a different selector!
// config.$diffNode defaults to #wikiDiff but is disabled below and never updated.
.
filter
(
(
selector
)
=>
selector
.
indexOf
(
'#wiki'
)
!==
0
);
loadingSelectors
.
push
(
'.ext-WikiEditor-realtimepreview-preview'
);
loadingSelectors
.
push
(
'.ext-WikiEditor-ManualWidget'
);
loadingSelectors
.
push
(
'.ext-WikiEditor-realtimepreview-ErrorLayout'
);
const
time
=
Date
.
now
();
this
.
pagePreview
.
doPreview
(
{
$textareaNode
:
$textareaNode
,
$previewNode
:
this
.
$previewNode
,
$spinnerNode
:
false
,
loadingSelectors
:
loadingSelectors
,
// Don't hide the diff view, if visible.
$diffNode
:
null
}
).
then
(
()
=>
{
this
.
errorLayout
.
toggle
(
false
);
},
(
code
,
result
)
=>
{
this
.
showError
(
(
new
mw
.
Api
()
).
getErrorMessage
(
result
)
);
mw
.
log
.
error
(
'WikiEditor realtime preview error'
,
result
);
}
).
then
(
()
=>
{
this
.
$loadingBar
.
hide
();
this
.
reloadButton
.
setDisabled
(
false
);
if
(
!
this
.
errorLayout
.
isVisible
()
)
{
// Only re-show the reload button if no error message is currently showing.
this
.
reloadButton
.
toggle
(
true
);
}
// Show the manual mode if applicable (but not if an error is displayed).
this
.
manualWidget
.
toggle
(
this
.
inManualMode
&&
!
this
.
errorLayout
.
isVisible
()
);
this
.
manualWidget
.
setDisabled
(
false
);
this
.
isPreviewing
=
false
;
this
.
checkResponseTimes
(
time
);
if
(
this
.
previewPending
)
{
this
.
previewPending
=
false
;
this
.
doRealtimePreview
();
}
mw
.
hook
(
'ext.WikiEditor.realtimepreview.loaded'
).
fire
(
this
);
}
);
};
module
.
exports
=
RealtimePreview
;
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Wed, Aug 19, 16:13 (4 w, 1 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
15/91/ff23cc83c89c6cdd2d4f2d9ab2be
Default Alt Text
RealtimePreview.js (13 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment