Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F4097971
controller.js
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
17 KB
Referenced Files
None
Subscribers
None
controller.js
View Options
'use strict'
;
function
Controller
(
target
)
{
// Mixin constructors
OO
.
EventEmitter
.
call
(
this
);
this
.
actionsByListener
=
{};
this
.
target
=
target
;
this
.
surface
=
null
;
this
.
listener
=
'onDocumentChange'
;
this
.
$highlights
=
$
(
'<div>'
);
this
.
dismissedFragments
=
{};
this
.
dismissedIds
=
{};
this
.
onDocumentChangeDebounced
=
ve
.
debounce
(
this
.
onDocumentChange
.
bind
(
this
),
100
);
this
.
onPositionDebounced
=
ve
.
debounce
(
this
.
onPosition
.
bind
(
this
),
100
);
this
.
onSelectDebounced
=
ve
.
debounce
(
this
.
onSelect
.
bind
(
this
),
100
);
this
.
onContextChangeDebounced
=
ve
.
debounce
(
this
.
onContextChange
.
bind
(
this
),
100
);
// Don't run a scroll if the previous animation is still running (which is jQuery 'fast' === 200ms)
this
.
scrollActionIntoViewDebounced
=
ve
.
debounce
(
this
.
scrollActionIntoView
.
bind
(
this
),
200
,
true
);
}
OO
.
mixinClass
(
Controller
,
OO
.
EventEmitter
);
Controller
.
prototype
.
setup
=
function
()
{
const
target
=
this
.
target
;
target
.
on
(
'surfaceReady'
,
()
=>
{
this
.
surface
=
target
.
getSurface
();
if
(
this
.
surface
.
getMode
()
!==
'visual'
)
{
// Some checks will entirely work in source mode for most cases.
// But others will fail spectacularly -- e.g. reference check
// isn't aware of <ref> tags and so will suggest that all content
// has references added. As such, disable in source mode for now.
return
;
}
if
(
!
this
.
editChecksArePossible
()
)
{
return
;
}
// ideally this would happen slightly earlier:
document
.
documentElement
.
classList
.
add
(
've-editcheck-available'
);
this
.
surface
.
getView
().
on
(
'position'
,
this
.
onPositionDebounced
);
this
.
surface
.
getModel
().
on
(
'undoStackChange'
,
this
.
onDocumentChangeDebounced
);
this
.
surface
.
getModel
().
on
(
'select'
,
this
.
onSelectDebounced
);
this
.
surface
.
getModel
().
on
(
'contextChange'
,
this
.
onContextChangeDebounced
);
this
.
on
(
'actionsUpdated'
,
this
.
onActionsUpdated
,
null
,
this
);
// Run on load (e.g. recovering from auto-save)
setTimeout
(
()
=>
this
.
onDocumentChange
(),
100
);
this
.
surface
.
on
(
'destroy'
,
()
=>
{
this
.
off
(
'actionsUpdated'
);
this
.
$highlights
.
empty
();
this
.
surface
=
null
;
this
.
actionsByListener
=
{};
this
.
dismissedFragments
=
{};
this
.
dismissedIds
=
{};
mw
.
editcheck
.
refCheckShown
=
false
;
}
);
},
null
,
this
);
target
.
on
(
'teardown'
,
()
=>
{
document
.
documentElement
.
classList
.
remove
(
've-editcheck-available'
);
},
null
,
this
);
this
.
setupPreSaveProcess
();
};
Controller
.
prototype
.
editChecksArePossible
=
function
()
{
return
[
'onBeforeSave'
,
'onDocumentChange'
].
some
(
(
listener
)
=>
mw
.
editcheck
.
editCheckFactory
.
getNamesByListener
(
listener
).
some
(
(
checkName
)
=>
{
const
check
=
mw
.
editcheck
.
editCheckFactory
.
create
(
checkName
,
this
,
mw
.
editcheck
.
config
[
checkName
]
);
return
check
.
canBeShown
();
}
)
);
};
Controller
.
prototype
.
updatePositions
=
function
()
{
this
.
drawSelections
();
this
.
drawGutter
();
this
.
emit
(
'position'
);
};
Controller
.
prototype
.
refresh
=
function
()
{
if
(
this
.
target
.
deactivating
||
!
this
.
target
.
active
)
{
return
;
}
if
(
this
.
listener
===
'onBeforeSave'
)
{
// These shouldn't be recalculated
this
.
emit
(
'actionsUpdated'
,
this
.
listener
,
this
.
getActions
(
this
.
listener
),
[],
[],
false
);
}
else
{
this
.
updateForListener
(
this
.
listener
,
true
);
}
};
Controller
.
prototype
.
updateForListener
=
function
(
listener
,
always
)
{
listener
=
listener
||
this
.
listener
;
const
existing
=
this
.
actionsByListener
[
listener
]
||
[];
const
actions
=
mw
.
editcheck
.
editCheckFactory
.
createAllByListener
(
this
,
listener
,
this
.
surface
.
getModel
()
)
.
map
(
(
action
)
=>
existing
.
find
(
(
oldAction
)
=>
oldAction
.
equals
(
action
)
)
||
action
);
this
.
actionsByListener
[
listener
]
=
actions
;
const
newActions
=
actions
.
filter
(
(
action
)
=>
existing
.
every
(
(
oldAction
)
=>
!
action
.
equals
(
oldAction
)
)
);
const
discardedActions
=
existing
.
filter
(
(
action
)
=>
actions
.
every
(
(
newAction
)
=>
!
action
.
equals
(
newAction
)
)
);
if
(
always
||
actions
.
length
!==
existing
.
length
||
newActions
.
length
||
discardedActions
.
length
)
{
this
.
emit
(
'actionsUpdated'
,
listener
,
actions
,
newActions
,
discardedActions
,
false
);
}
return
actions
;
};
Controller
.
prototype
.
removeAction
=
function
(
listener
,
action
,
rejected
)
{
const
actions
=
this
.
getActions
(
listener
);
const
index
=
actions
.
indexOf
(
action
);
if
(
index
===
-
1
)
{
return
;
}
const
removed
=
actions
.
splice
(
index
,
1
);
if
(
action
===
this
.
focused
)
{
this
.
focused
=
undefined
;
}
this
.
emit
(
'actionsUpdated'
,
listener
,
actions
,
[],
removed
,
rejected
);
};
Controller
.
prototype
.
focusAction
=
function
(
action
,
scrollTo
)
{
if
(
!
scrollTo
&&
action
===
this
.
focused
)
{
// Don't emit unnecessary events if there is no change or scroll
return
;
}
this
.
focused
=
action
;
this
.
emit
(
'focusAction'
,
action
,
this
.
getActions
().
indexOf
(
action
),
scrollTo
);
this
.
updatePositions
();
};
Controller
.
prototype
.
getActions
=
function
(
listener
)
{
return
this
.
actionsByListener
[
listener
||
this
.
listener
]
||
[];
};
Controller
.
prototype
.
onSelect
=
function
(
selection
)
{
if
(
!
this
.
surface
)
{
// This is debounced, and could potentially be called after teardown
return
;
}
if
(
this
.
surface
.
getView
().
reviewMode
)
{
// In review mode the selection and display of checks is being managed by the dialog
return
;
}
if
(
OO
.
ui
.
isMobile
()
)
{
// On mobile we want to close the drawer if the keyboard is shown
if
(
this
.
surface
.
getView
().
hasNativeCursorSelection
()
)
{
// A native cursor selection means the keyboard will be visible
this
.
closeDialog
(
'mobile-keyboard'
);
}
}
if
(
this
.
getActions
().
length
===
0
||
selection
.
isNull
()
)
{
// Nothing to do
return
;
}
const
actions
=
this
.
getActions
().
filter
(
(
check
)
=>
check
.
getHighlightSelections
().
some
(
(
highlight
)
=>
highlight
.
getCoveringRange
().
containsRange
(
selection
.
getCoveringRange
()
)
)
);
this
.
focusAction
(
actions
[
0
]
||
null
,
false
);
};
Controller
.
prototype
.
onContextChange
=
function
()
{
if
(
!
this
.
surface
)
{
// This is debounced, and could potentially be called after teardown
return
;
}
if
(
OO
.
ui
.
isMobile
()
&&
this
.
surface
.
getContext
().
isVisible
()
)
{
// The context overlaps the drawer on mobile, so we should get rid of the drawer
this
.
closeDialog
(
'context'
);
}
};
Controller
.
prototype
.
onPosition
=
function
()
{
if
(
!
this
.
surface
)
{
// This is debounced, and could potentially be called after teardown
return
;
}
this
.
updatePositions
();
if
(
this
.
getActions
().
length
&&
this
.
focused
&&
this
.
surface
.
getView
().
reviewMode
)
{
this
.
scrollActionIntoViewDebounced
(
this
.
focused
);
}
};
Controller
.
prototype
.
onDocumentChange
=
function
()
{
if
(
!
this
.
surface
)
{
// This is debounced, and could potentially be called after teardown
return
;
}
if
(
this
.
listener
!==
'onBeforeSave'
)
{
this
.
updateForListener
(
'onDocumentChange'
);
}
this
.
updatePositions
();
};
Controller
.
prototype
.
onActionsUpdated
=
function
(
listener
,
actions
,
newActions
,
discardedActions
)
{
// do we need to redraw anything?
if
(
newActions
.
length
||
discardedActions
.
length
)
{
if
(
this
.
focused
&&
discardedActions
.
includes
(
this
.
focused
)
)
{
this
.
focused
=
undefined
;
}
this
.
updatePositions
();
}
// do we need to show mid-edit actions?
if
(
listener
!==
'onDocumentChange'
)
{
return
;
}
if
(
!
actions
.
length
)
{
return
;
}
const
windowName
=
OO
.
ui
.
isMobile
()
?
'gutterSidebarEditCheckDialog'
:
'sidebarEditCheckDialog'
;
let
shownPromise
;
const
currentWindow
=
this
.
surface
.
getSidebarDialogs
().
getCurrentWindow
();
if
(
!
currentWindow
||
currentWindow
.
constructor
.
static
.
name
!==
windowName
)
{
this
.
target
.
$element
.
addClass
(
've-ui-editCheck-sidebar-active'
);
const
windowAction
=
ve
.
ui
.
actionFactory
.
create
(
'window'
,
this
.
surface
,
'check'
);
shownPromise
=
windowAction
.
open
(
windowName
,
{
listener
:
listener
,
actions
:
actions
,
controller
:
this
}
).
then
(
(
instance
)
=>
{
ve
.
track
(
'activity.editCheckDialog'
,
{
action
:
'window-open-from-check-midedit'
}
);
instance
.
closed
.
then
(
()
=>
{
this
.
target
.
$element
.
removeClass
(
've-ui-editCheck-sidebar-active'
);
}
);
}
);
}
else
{
shownPromise
=
ve
.
createDeferred
().
resolve
().
promise
();
}
shownPromise
.
then
(
()
=>
{
newActions
.
forEach
(
(
action
)
=>
{
ve
.
track
(
'activity.editCheck-'
+
action
.
getName
(),
{
action
:
'check-shown-midedit'
}
);
}
);
}
);
};
Controller
.
prototype
.
setupPreSaveProcess
=
function
()
{
const
target
=
this
.
target
;
const
preSaveProcess
=
target
.
getPreSaveProcess
();
preSaveProcess
.
next
(
()
=>
{
const
surface
=
target
.
getSurface
();
if
(
surface
.
getMode
()
!==
'visual'
)
{
return
;
}
ve
.
track
(
'counter.editcheck.preSaveChecksAvailable'
);
const
oldFocused
=
this
.
focused
;
this
.
listener
=
'onBeforeSave'
;
const
actions
=
this
.
updateForListener
(
'onBeforeSave'
);
if
(
actions
.
length
)
{
ve
.
track
(
'counter.editcheck.preSaveChecksShown'
);
mw
.
editcheck
.
refCheckShown
=
mw
.
editcheck
.
refCheckShown
||
actions
.
some
(
(
action
)
=>
action
.
getName
()
===
'addReference'
);
this
.
setupToolbar
(
target
);
let
$contextContainer
,
contextPadding
;
if
(
surface
.
context
.
popup
)
{
contextPadding
=
surface
.
context
.
popup
.
containerPadding
;
$contextContainer
=
surface
.
context
.
popup
.
$container
;
surface
.
context
.
popup
.
$container
=
surface
.
$element
;
surface
.
context
.
popup
.
containerPadding
=
20
;
}
return
this
.
closeSidebars
(
'preSaveProcess'
).
then
(
()
=>
this
.
closeDialog
(
'preSaveProcess'
).
then
(
()
=>
{
this
.
originalToolbar
.
toggle
(
false
);
target
.
onContainerScroll
();
const
windowAction
=
ve
.
ui
.
actionFactory
.
create
(
'window'
,
surface
,
'check'
);
return
windowAction
.
open
(
'fixedEditCheckDialog'
,
{
listener
:
'onBeforeSave'
,
actions
:
actions
,
controller
:
this
}
)
.
then
(
(
instance
)
=>
{
ve
.
track
(
'activity.editCheckDialog'
,
{
action
:
'window-open-from-check-presave'
}
);
actions
.
forEach
(
(
action
)
=>
{
ve
.
track
(
'activity.editCheck-'
+
action
.
getName
(),
{
action
:
'check-shown-presave'
}
);
}
);
instance
.
closed
.
then
(
()
=>
{},
()
=>
{}
).
then
(
()
=>
{
surface
.
getView
().
setReviewMode
(
false
);
this
.
listener
=
'onDocumentChange'
;
this
.
focused
=
oldFocused
;
// Re-open the mid-edit sidebar if necessary.
this
.
refresh
();
}
);
return
instance
.
closing
.
then
(
(
data
)
=>
{
if
(
target
.
deactivating
||
!
target
.
active
)
{
// Someone clicking "read" to leave the article
// will trigger the closing of this; in that
// case, just abandon what we're doing
return
ve
.
createDeferred
().
reject
().
promise
();
}
this
.
restoreToolbar
(
target
);
if
(
$contextContainer
)
{
surface
.
context
.
popup
.
$container
=
$contextContainer
;
surface
.
context
.
popup
.
containerPadding
=
contextPadding
;
}
target
.
onContainerScroll
();
if
(
data
)
{
const
delay
=
ve
.
createDeferred
();
// If they inserted, wait 2 seconds on desktop
// before showing save dialog to give user time
// to see success notification.
setTimeout
(
()
=>
{
ve
.
track
(
'counter.editcheck.preSaveChecksCompleted'
);
delay
.
resolve
();
},
!
OO
.
ui
.
isMobile
()
&&
data
.
action
!==
'reject'
?
2000
:
0
);
return
delay
.
promise
();
}
else
{
// closed via "back" or otherwise
ve
.
track
(
'counter.editcheck.preSaveChecksAbandoned'
);
return
ve
.
createDeferred
().
reject
().
promise
();
}
}
);
}
);
}
)
);
}
else
{
this
.
listener
=
'onDocumentChange'
;
// Counterpart to earlier preSaveChecksShown, for use in tracking
// errors in check-generation:
ve
.
track
(
'counter.editcheck.preSaveChecksNotShown'
);
}
}
);
};
Controller
.
prototype
.
setupToolbar
=
function
(
target
)
{
const
surface
=
target
.
getSurface
();
const
toolbar
=
target
.
getToolbar
();
const
reviewToolbar
=
new
ve
.
ui
.
PositionedTargetToolbar
(
target
,
target
.
toolbarConfig
);
reviewToolbar
.
setup
(
[
{
name
:
'back'
,
type
:
'bar'
,
include
:
[
'editCheckBack'
]
},
// Placeholder toolbar groups
// TODO: Make a proper TitleTool?
{
name
:
'title'
,
type
:
'bar'
,
include
:
[]
},
{
name
:
'save'
,
// TODO: MobileArticleTarget should ignore 'align'
align
:
OO
.
ui
.
isMobile
()
?
'before'
:
'after'
,
type
:
'bar'
,
include
:
[
'showSaveDisabled'
]
}
],
surface
);
reviewToolbar
.
$element
.
addClass
(
've-ui-editCheck-toolbar'
);
reviewToolbar
.
items
[
1
].
$element
.
removeClass
(
'oo-ui-toolGroup-empty'
);
reviewToolbar
.
items
[
1
].
$group
.
append
(
$
(
'<span>'
).
addClass
(
've-ui-editCheck-toolbar-title'
).
text
(
ve
.
msg
(
'editcheck-dialog-title'
)
)
);
if
(
OO
.
ui
.
isMobile
()
)
{
reviewToolbar
.
$element
.
addClass
(
've-init-mw-mobileArticleTarget-toolbar'
);
}
target
.
toolbar
.
$element
.
before
(
reviewToolbar
.
$element
);
target
.
toolbar
=
reviewToolbar
;
reviewToolbar
.
initialize
();
this
.
originalToolbar
=
toolbar
;
this
.
reviewToolbar
=
reviewToolbar
;
};
Controller
.
prototype
.
restoreToolbar
=
function
(
target
)
{
if
(
!
this
.
reviewToolbar
)
{
return
;
}
this
.
reviewToolbar
.
$element
.
remove
();
this
.
originalToolbar
.
toggle
(
true
);
target
.
toolbar
=
this
.
originalToolbar
;
// Creating a new PositionedTargetToolbar stole the
// toolbar windowmanagers, so we need to make the
// original toolbar reclaims them:
target
.
setupToolbar
(
target
.
getSurface
()
);
// If the window was resized while the originalToolbar was hidden then
// the cached measurements will be wrong. Recalculate.
this
.
originalToolbar
.
onWindowResize
();
this
.
reviewToolbar
=
null
;
this
.
originalToolbar
=
null
;
};
Controller
.
prototype
.
drawSelections
=
function
()
{
const
surfaceView
=
this
.
surface
.
getView
();
if
(
this
.
focused
)
{
// The currently-focused check gets a selection:
// TODO: clicking the selection should activate the sidebar-action
surfaceView
.
getSelectionManager
().
drawSelections
(
'editCheckWarning'
,
this
.
focused
.
getHighlightSelections
().
map
(
(
selection
)
=>
ve
.
ce
.
Selection
.
static
.
newFromModel
(
selection
,
surfaceView
)
)
);
}
else
{
surfaceView
.
getSelectionManager
().
drawSelections
(
'editCheckWarning'
,
[]
);
}
if
(
this
.
listener
===
'onBeforeSave'
)
{
// Review mode grays out everything that's not highlighted:
const
highlightNodes
=
[];
this
.
getActions
().
forEach
(
(
action
)
=>
{
action
.
getHighlightSelections
().
forEach
(
(
selection
)
=>
{
highlightNodes
.
push
.
apply
(
highlightNodes
,
surfaceView
.
getDocument
().
selectNodes
(
selection
.
getCoveringRange
(),
'branches'
).
map
(
(
spec
)
=>
spec
.
node
)
);
}
);
}
);
surfaceView
.
setReviewMode
(
true
,
highlightNodes
);
}
};
Controller
.
prototype
.
drawGutter
=
function
()
{
if
(
OO
.
ui
.
isMobile
()
)
{
return
;
}
this
.
$highlights
.
empty
();
const
actions
=
this
.
getActions
();
if
(
actions
.
length
===
0
)
{
return
;
}
const
surfaceView
=
this
.
surface
.
getView
();
actions
.
forEach
(
(
action
)
=>
{
action
.
top
=
Infinity
;
action
.
getHighlightSelections
().
forEach
(
(
selection
)
=>
{
const
selectionView
=
ve
.
ce
.
Selection
.
static
.
newFromModel
(
selection
,
surfaceView
);
const
rect
=
selectionView
.
getSelectionBoundingRect
();
if
(
!
rect
)
{
return
;
}
// The following classes are used here:
// * ve-ui-editCheck-gutter-highlight-error
// * ve-ui-editCheck-gutter-highlight-warning
// * ve-ui-editCheck-gutter-highlight-notice
// * ve-ui-editCheck-gutter-highlight-success
// * ve-ui-editCheck-gutter-highlight-active
// * ve-ui-editCheck-gutter-highlight-inactive
this
.
$highlights
.
append
(
$
(
'<div>'
)
.
addClass
(
've-ui-editCheck-gutter-highlight'
)
.
addClass
(
've-ui-editCheck-gutter-highlight-'
+
action
.
getType
()
)
.
addClass
(
've-ui-editCheck-gutter-highlight-'
+
(
action
===
this
.
focused
?
'active'
:
'inactive'
)
)
.
css
(
{
top
:
rect
.
top
-
2
,
height
:
rect
.
height
+
4
}
)
.
on
(
'click'
,
()
=>
this
.
focusAction
(
action
)
)
);
action
.
top
=
Math
.
min
(
action
.
top
,
rect
.
top
);
}
);
}
);
surfaceView
.
appendHighlights
(
this
.
$highlights
,
false
);
};
Controller
.
prototype
.
scrollActionIntoView
=
function
(
action
)
{
// scrollSelectionIntoView scrolls to the focus of a selection, but we
// want the very beginning to be in view, so collapse it:
const
selection
=
action
.
getHighlightSelections
()[
0
].
collapseToStart
();
const
padding
=
{
top
:
OO
.
ui
.
isMobile
()
?
80
:
action
.
widget
.
$element
[
0
].
getBoundingClientRect
().
top
,
bottom
:
20
};
if
(
ve
.
ui
.
FixedEditCheckDialog
.
static
.
position
===
'below'
)
{
// TODO: ui.surface getPadding should really be fixed for this
const
currentWindow
=
this
.
surface
.
getToolbarDialogs
(
ve
.
ui
.
FixedEditCheckDialog
.
static
.
position
).
getCurrentWindow
();
if
(
currentWindow
)
{
padding
.
bottom
+=
currentWindow
.
getContentHeight
();
}
}
this
.
surface
.
scrollSelectionIntoView
(
selection
,
{
animate
:
true
,
padding
:
padding
,
alignToTop
:
true
}
);
};
Controller
.
prototype
.
closeDialog
=
function
(
action
)
{
this
.
focusAction
(
undefined
);
const
windowAction
=
ve
.
ui
.
actionFactory
.
create
(
'window'
,
this
.
surface
,
'check'
);
return
windowAction
.
close
(
'fixedEditCheckDialog'
,
action
?
{
action
:
action
}
:
undefined
).
closed
.
then
(
()
=>
{},
()
=>
{}
);
};
Controller
.
prototype
.
closeSidebars
=
function
(
action
)
{
const
currentWindow
=
this
.
surface
.
getSidebarDialogs
().
getCurrentWindow
();
if
(
currentWindow
)
{
// .always is not chainable
return
currentWindow
.
close
(
action
?
{
action
:
action
}
:
undefined
).
closed
.
then
(
()
=>
{},
()
=>
{}
);
}
return
ve
.
createDeferred
().
resolve
().
promise
();
};
module
.
exports
=
{
Controller
:
Controller
};
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Tue, Aug 18, 15:08 (3 w, 6 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
0b/3e/8de56cddbaa80d32de4e2adfc54c
Default Alt Text
controller.js (17 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment