Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F5963517
ve.ui.MWExtensionWindow.js
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
8 KB
Referenced Files
None
Subscribers
None
ve.ui.MWExtensionWindow.js
View Options
/*!
* VisualEditor UserInterface MWExtensionWindow class.
*
* @copyright See AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* Mixin for windows for editing generic MediaWiki extensions.
*
* @class
* @abstract
*
* @constructor
* @param {Object} [config] Configuration options
*/
ve
.
ui
.
MWExtensionWindow
=
function
VeUiMWExtensionWindow
()
{
this
.
whitespace
=
null
;
this
.
input
=
null
;
this
.
originalMwData
=
null
;
this
.
onChangeHandler
=
ve
.
debounce
(
this
.
onChange
.
bind
(
this
)
);
};
/* Inheritance */
OO
.
initClass
(
ve
.
ui
.
MWExtensionWindow
);
/* Static properties */
/**
* Extension is allowed to have empty contents
*
* @static
* @property {boolean}
* @inheritable
*/
ve
.
ui
.
MWExtensionWindow
.
static
.
allowedEmpty
=
false
;
/**
* Tell Parsoid to self-close tags when the body is empty
*
* i.e. `<foo></foo>` -> `<foo/>`
*
* @static
* @property {boolean}
* @inheritable
*/
ve
.
ui
.
MWExtensionWindow
.
static
.
selfCloseEmptyBody
=
false
;
/**
* Inspector's directionality, 'ltr' or 'rtl'
*
* Leave as null to use the directionality of the current fragment.
*
* @static
* @property {string|null}
* @inheritable
*/
ve
.
ui
.
MWExtensionWindow
.
static
.
dir
=
null
;
/* Methods */
/**
* @inheritdoc OO.ui.Window
*/
ve
.
ui
.
MWExtensionWindow
.
prototype
.
initialize
=
function
()
{
this
.
input
=
new
ve
.
ui
.
WhitespacePreservingTextInputWidget
(
{
limit
:
1
,
classes
:
[
've-ui-mwExtensionWindow-input'
]
}
);
};
/**
* Get the placeholder text for the content input area.
*
* @return {string} Placeholder text
*/
ve
.
ui
.
MWExtensionWindow
.
prototype
.
getInputPlaceholder
=
function
()
{
return
''
;
};
/**
* @inheritdoc OO.ui.Window
*/
ve
.
ui
.
MWExtensionWindow
.
prototype
.
getSetupProcess
=
function
(
data
,
process
)
{
data
=
data
||
{};
return
process
.
next
(
()
=>
{
// Initialization
this
.
whitespace
=
[
''
,
''
];
if
(
this
.
selectedNode
)
{
const
mwData
=
this
.
selectedNode
.
getAttribute
(
'mw'
);
// mwData.body can be null in <selfclosing/> extensions
this
.
input
.
setValueAndWhitespace
(
(
mwData
.
body
&&
mwData
.
body
.
extsrc
)
||
''
);
this
.
originalMwData
=
mwData
;
}
else
{
if
(
!
this
.
constructor
.
static
.
modelClasses
[
0
].
static
.
isContent
)
{
// New nodes should use linebreaks for blocks
this
.
input
.
setWhitespace
(
[
'\n'
,
'\n'
]
);
}
this
.
input
.
setValue
(
''
);
}
this
.
input
.
$input
.
attr
(
'placeholder'
,
this
.
getInputPlaceholder
()
);
const
dir
=
this
.
constructor
.
static
.
dir
||
data
.
dir
;
this
.
input
.
setDir
(
dir
);
this
.
input
.
setReadOnly
(
this
.
isReadOnly
()
);
this
.
actions
.
setAbilities
(
{
done
:
false
}
);
this
.
input
.
connect
(
this
,
{
change
:
'onChangeHandler'
}
);
}
);
};
/**
* @inheritdoc OO.ui.Window
*/
ve
.
ui
.
MWExtensionWindow
.
prototype
.
getReadyProcess
=
function
(
data
,
process
)
{
return
process
;
};
/**
* @inheritdoc OO.ui.Window
*/
ve
.
ui
.
MWExtensionWindow
.
prototype
.
getTeardownProcess
=
function
(
data
,
process
)
{
return
process
.
next
(
()
=>
{
// Don't hold on to the original data, it's only refreshed on setup for existing nodes
this
.
originalMwData
=
null
;
this
.
input
.
disconnect
(
this
,
{
change
:
'onChangeHandler'
}
);
}
);
};
/**
* @inheritdoc OO.ui.Dialog
*/
ve
.
ui
.
MWExtensionWindow
.
prototype
.
getActionProcess
=
function
(
action
,
process
)
{
return
process
.
first
(
()
=>
{
if
(
action
===
'done'
)
{
if
(
this
.
constructor
.
static
.
allowedEmpty
||
this
.
input
.
getValue
()
!==
''
)
{
this
.
insertOrUpdateNode
();
}
else
if
(
this
.
selectedNode
&&
!
this
.
constructor
.
static
.
allowedEmpty
)
{
// Content has been emptied on a node which isn't allowed to
// be empty, so delete it.
this
.
removeNode
();
}
}
}
);
};
/**
* Handle change event.
*/
ve
.
ui
.
MWExtensionWindow
.
prototype
.
onChange
=
function
()
{
this
.
updateActions
();
};
/**
* Update the 'done' action according to whether there are changes
*/
ve
.
ui
.
MWExtensionWindow
.
prototype
.
updateActions
=
function
()
{
this
.
actions
.
setAbilities
(
{
done
:
this
.
isSaveable
()
}
);
};
/**
* Check if mwData would be modified if window contents were applied.
* This is used to determine if it's meaningful for the user to save the
* contents into the document; this is likely true of newly-created elements.
*
* @return {boolean} mwData would be modified
*/
ve
.
ui
.
MWExtensionWindow
.
prototype
.
isSaveable
=
function
()
{
let
modified
;
if
(
this
.
originalMwData
)
{
const
mwDataCopy
=
ve
.
copy
(
this
.
originalMwData
);
this
.
updateMwData
(
mwDataCopy
);
modified
=
!
ve
.
compare
(
this
.
originalMwData
,
mwDataCopy
);
}
else
{
modified
=
true
;
}
return
modified
;
};
/**
* @deprecated Moved to ve.ui.MWExtensionWindow.prototype.isSaveable
* @return {boolean} mwData would be modified
*/
ve
.
ui
.
MWExtensionWindow
.
prototype
.
isModified
=
ve
.
ui
.
MWExtensionWindow
.
prototype
.
isSaveable
;
/**
* Check if mwData has meaningful edits. This is used to determine if it's
* meaningful to warn the user before closing the dialog without saving. Unlike
* `isModified()` above, we consider a newly-created but unmodified element to
* be non-meaningful because the user can simply re-open the dialog to restore
* their state.
*
* @return {boolean} mwData would contain new user input
*/
ve
.
ui
.
MWExtensionWindow
.
prototype
.
hasMeaningfulEdits
=
function
()
{
let
mwDataBaseline
;
if
(
this
.
originalMwData
)
{
mwDataBaseline
=
this
.
originalMwData
;
}
else
{
mwDataBaseline
=
this
.
getNewElement
().
attributes
.
mw
;
}
const
mwDataCopy
=
ve
.
copy
(
mwDataBaseline
);
this
.
updateMwData
(
mwDataCopy
);
// We have some difficulty here. `updateMwData()` in this class calls on
// `this.input.getValueAndWhitespace()`. The 'and whitespace' means that
// we cannot directly compare a new element's mwData with a newly-opened
// dialog's mwData because it may have additional newlines.
// We don't want to touch `this.input` or `prototype.updateMwData` because
// they're overridden in subclasses. Therefore, we consider whitespace-only
// changes to a new element to be non-meaningful too.
const
changed
=
OO
.
getProp
(
mwDataCopy
,
'body'
,
'extsrc'
);
if
(
changed
!==
undefined
)
{
OO
.
setProp
(
mwDataCopy
,
'body'
,
'extsrc'
,
changed
.
trim
()
);
}
// Also trim the baseline. In "edit" mode we likely have added whitespace,
// and in "insert" mode we don't want to break if the default value starts
// or ends with whitespace.
const
baselineChanged
=
OO
.
getProp
(
mwDataBaseline
,
'body'
,
'extsrc'
);
if
(
baselineChanged
!==
undefined
)
{
OO
.
setProp
(
mwDataBaseline
,
'body'
,
'extsrc'
,
baselineChanged
.
trim
()
);
}
return
!
ve
.
compare
(
mwDataBaseline
,
mwDataCopy
);
};
/**
* Create an new data element for the model class associated with this inspector
*
* @return {Object} Element data
*/
ve
.
ui
.
MWExtensionWindow
.
prototype
.
getNewElement
=
function
()
{
// Extension inspectors which create elements should either match
// a single modelClass or override this method.
const
modelClass
=
this
.
constructor
.
static
.
modelClasses
[
0
];
return
{
type
:
modelClass
.
static
.
name
,
attributes
:
{
mw
:
{
name
:
modelClass
.
static
.
extensionName
,
attrs
:
{},
body
:
{
extsrc
:
''
}
}
}
};
};
/**
* Insert or update the node in the document model from the new values
*/
ve
.
ui
.
MWExtensionWindow
.
prototype
.
insertOrUpdateNode
=
function
()
{
const
surfaceModel
=
this
.
getFragment
().
getSurface
();
if
(
this
.
selectedNode
)
{
const
mwData
=
ve
.
copy
(
this
.
selectedNode
.
getAttribute
(
'mw'
)
);
this
.
updateMwData
(
mwData
);
surfaceModel
.
change
(
ve
.
dm
.
TransactionBuilder
.
static
.
newFromAttributeChanges
(
surfaceModel
.
getDocument
(),
this
.
selectedNode
.
getOuterRange
().
start
,
{
mw
:
mwData
}
)
);
}
else
{
const
element
=
this
.
getNewElement
();
this
.
updateMwData
(
element
.
attributes
.
mw
);
// Collapse returns a new fragment, so update this.fragment
this
.
fragment
=
this
.
getFragment
().
collapseToEnd
();
this
.
getFragment
().
insertContent
(
[
element
,
{
type
:
'/'
+
element
.
type
}
]
);
}
};
/**
* Remove the node form the document model
*/
ve
.
ui
.
MWExtensionWindow
.
prototype
.
removeNode
=
function
()
{
this
.
getFragment
().
removeContent
();
};
/**
* Update mwData object with the new values from the inspector or dialog
*
* @param {Object} mwData MediaWiki data object
*/
ve
.
ui
.
MWExtensionWindow
.
prototype
.
updateMwData
=
function
(
mwData
)
{
const
tagName
=
mwData
.
name
;
let
value
=
this
.
input
.
getValueAndWhitespace
();
// XML-like tags in wikitext are not actually XML and don't expect their contents to be escaped.
// This means that it is not possible for a tag '<foo>…</foo>' to contain the string '</foo>'.
// Prevent that by escaping the first angle bracket '<' to '<'. (T59429)
value
=
value
.
replace
(
new
RegExp
(
'<(/'
+
tagName
+
'\\s*>)'
,
'gi'
),
'<$1'
);
if
(
value
.
trim
()
===
''
&&
this
.
constructor
.
static
.
selfCloseEmptyBody
)
{
delete
mwData
.
body
;
}
else
{
mwData
.
body
=
mwData
.
body
||
{};
mwData
.
body
.
extsrc
=
value
;
}
};
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sat, Oct 3, 09:41 (1 w, 3 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
ff/f4/c4466844a5d899cf43f3903daf1a
Default Alt Text
ve.ui.MWExtensionWindow.js (8 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment