-
Notifications
You must be signed in to change notification settings - Fork 20
Howto dev
Vivaldi is built on top of React - an UI creating library. Vivaldi source code consists of lots of modules combined by Webpack into a few js-files. VivaldiHooks provides tools for replacing React-components and modules' exports, giving mod developers a way to change not only the look but also the behavior hidden deep inside.
There is a project which may help research Vivaldi internals: bundleutils. In order to set it up you need to install Node.js and then execute the following command from the project's directory:
npm install
To do the work, copy original Vivaldi js files into the folder and run
node split.js
It creates folders with a modules saved to separate files, most of them have meaningful names. Inside the files arguments of require function (minified name in the most cases is n
or a
) - module numbers - are complemented by comments with module names if found. Example:
var i = a(0 /* React_wrapper */),
s = a(10 /* PageActions */),
n = a(2 /* _getLocalizedMessage */),
r = a(126 /* settings_keywordFound */),
o = a(97),
If you use source code editor with refactoring abilities it may be useful rename such variables into module names - modules after that are easier to understand
}), d(this, "openPrivacyPage", e => {
PageActions.a.openURL(u), e.preventDefault()
}), this.state = {
Also if some module exports svg image the image is saved as a separate svg file.
So file names contain names which may be used by mods to reuse or modify modules. For examples presence of file jsout_name/React.js
means that a mod may use "React" module:
const ReactModule = vivaldi.jdhooks.require("React")
//...
return ReactModule.createElement("div", {}, "Text")
or modify it
vivaldi.hookModule("React", //..
It's a little bit harder with React-classes.
class n extends React.PureComponent {
render() {
const {
children: e,
filter: t
} = this.props;
return t ? Array.isArray(e) ? e.map((e, a) => React.createElement(React.Fragment, {
key: a,
__source: {
fileName: "C:\\new-bot\\new-builder\\workers\\ow32\\build\\vivaldi\\vivapp\\src\\components\\settings\\SettingsSearchCategoryChild.jsx",
lineNumber: 18
}
}, Object(s.a)(e, t))) : Object(s.a)(e, t) : e
}
}
VivaldiHooks uses fileName
value to identify classes. Value part between "component" and ".jsx" with all \\
and /
replaced by _
is used as a class name. In the example above class name would be "settings_SettingsSearchCategoryChild". It must be noted it's more often fileName
is placed outside of the class, in that case it's defined as a variable before the class definition. Example:
var i = a(117 /* common_Favicon */),
s = a(0 /* React_wrapper */),
n = a.n(s),
r = a(13 /* _VivaldiIcons */),
o = "C:\\new-bot\\new-builder\\workers\\ow32\\build\\vivaldi\\vivapp\\src\\components\\tabs\\LoadingFavicon.jsx";
class l extends n.a.PureComponent {
When a module contains several React-classes it may be not so easy to understand which filename variable describes one or another class, so use refactoring to make it clearer.
vivaldi.jdhooks.addStyle
(style, /*optional*/
description)
Used to add css styles directly from js mod.
vivaldi.jdhooks.addStyle(`
.quick-command:not([data-selected]) .quick-command-close-tab { display: none }
.quick-command[data-selected] .quick-command-close-tab { background-color: rgba(0, 0, 0, .15)}
`, "qc-close-tab.js")
Optional description
parameter is put to "description" argument of style
tag in the page's header.
vivaldi.jdhooks.hookClass
(className, /*class => newClass*/
callback)
The main function for overriding React-classes. callback
function receives old (or overrided by another mod) class as argument and should return a new/updated one.
vivaldi.jdhooks.hookClass("find-in-page_FindInPage", cls => {
class newFindInPage extends cls {
constructor(...e) {
super(...e)
this.populateWithPageSelection = () => { this.focusFindInPageInput() }
}
}
return newFindInPage
})
vivaldi.jdhooks.hookModule
(moduleName, /*(moduleInfo, exports) => newExports*/
callback)
It calls callback
after moduleName
is initialized, allowing to override module's exports to change its behavior. callback
should return new/overriden exports. Example: adding new default value for settings parameter:
vivaldi.jdhooks.hookModule("vivaldiSettings", (moduleInfo, exports) => {
let oldGetDefault = exports.getDefault
exports.getDefault = name => {
switch (name) {
case "BOOKMARK_BUTTON_POSITION": return "value"
default: return oldGetDefault(name)
}
}
return exports
})
`hookModule` is deprecated and will be removed in the nearest future
vivaldiSettings
module exports an object implementing several functions, one of them - getDefault
- returns default value for a specified parameter.
vivaldi.jdhooks.require
(moduleId, exportName)
It returns exports of specified module. It accepts both integer value as module number (may be useful for debugging) and string value as module name. This is the main way to reuse Vivaldi components.
const UrlFieldActions = vivaldi.jdhooks.require('_UrlFieldActions')
UrlFieldActions.go(["https://vivaldi.com"], {
inCurrent: false,
addTypedHistory: true,
addTypedSearchHistory: false,
enableSearch: true
})
Sometimes a module exports several entities (i.e. several different modules combined by ModuleConcatenationPlugin
webpack-plugin). exportName
parameter allows to choose which of them is required. Default value is "default".
If exported value is not an object the parameter is ignored and require
returns the value as is.
If exportName
is equal to "*" require
returns exported value as is.
If exportName
matches one of exported object's properties name require
returns the property.
If exportName
is equal to "default" (or not specified) and a module exports an object with a single property require
returns the property.
Warning: require
may be called only from VivaldiHooks API callbacks or from functions called only from these callbacs. This is because require
forces module's dependencies to be initialized, so they may be loaded before mods calling hookModule for them.
vivaldi.jdhooks.insertWatcher
(class, /*{settings:[], prefs:[]}*/
params)
Extends class
to make it watch for settings/prefs changes. Settings (see module vivaldiSettings
) will be available through this.state.jdVivaldiSettings.{keyname}
, prefs (PrefsCache
) - this.state.jdPrefs["{keyname}"]
. Example:
return vivaldi.jdhooks.insertWatcher(downloadTabWebpageContent,
{
settings: ["SHOW_DOWNLOADTAB_FOR_NEW_DOWNLOADS"],
prefs: ["vivaldi.tabs.visible", "vivaldi.clock.mode"]
})
Inside class:
const val1 = this.state.jdVivaldiSettings.SHOW_DOWNLOADTAB_FOR_NEW_DOWNLOADS
const val2 = this.state.jdPrefs["vivaldi.tabs.visible"]
Don't pass function's result to React.createElement directly, store it as a constant outside of render
or even your hooked class. Use _PrefKeys
module to access pref names.
vivaldi.jdhooks.onUIReady
(callback)
Legacy method used for old style mods working through DOM event listeners. callback
is called after Vivaldi UI is initialized and mounted to DOM.
vivaldi.jdhooks.onUIReady(() => {
document.addEventListener("mouseup", event => {
React works with Virtual DOM, initialized components may not exist in the real DOM for some time after rendering. Take a look at React component lifecycle explanation.
DOM in React-application is a bunch of nested into each other elements created by React.createElement call.
render() {
return React.createElement(settings_SettingsSearchCategoryChild, { filter: this.props.filter },
React.createElement("h2", null, "Hooks"),
React.createElement("div", { className: "setting-group unlimited" },
React.createElement("div", { className: "setting-single" },
React.createElement("label", null,
React.createElement("input", {
type: "checkbox",
checked: this.state.defaultLoad,
onChange: this.toggleDefaultLoad.bind(this)
}),
React.createElement("span", null, "Startup mode for new items")
)
),
React.createElement("div", {
className: "setting-group unlimited pad-top"
},
The most demonstrative way to modify React-class is changing result of parent class' render
call. For that result's .props.children
value should be modified. I recommend to hook some React class and override its render
this way to learn how it looks like in the console:
render()
{
let r = super.render() //call original render
console.log("new render", {this: this, rendered: r})
return r
}
When you implement componentDidMount
or componentWillUnmount
in overridden class check existence of these method in the parent class and call them if they exist. It will help avoiding bugs if two mods implement these methods for the same class.
componentWillUnmount() {
//do some work
//...
if (super.componentWillUnmount) super.componentWillUnmount()
}
Sometimes class methods are defined not statically (as class members) but dinamically at runtime (as newly created object's members). You can override such methods only at runtime. Here is a part of original class implementation using this way (it looks ugly because js-beautify haven't managed to format it better):
}), fe(this, "attachEventHandlers", () => {
q.a.addChangeListener(this._onWebpageviewStoreChanged)
}), fe(this, "removeEventHandlers", () => {
q.a.removeChangeListener(this._onWebpageviewStoreChanged)
}), fe(this, "attachWebviewHandlers", () => {
Object.keys(_e).forEach(e => {
fe
in this example is a wrapper around Object.defineProperty
. Here is an example of overriding a class member defined at runtime:
class newClass extends oldClass {
constructor(...e) {
super(...e)
const old_handleLoadStop = this.handleLoadStop
this.handleLoadStop = e => {
//do some work
//...
return old_handleLoadStop(e)
}
If your render implementation depends of values original render doesn't depend of (f.e. you want to display current time in a component), it's good to store these values to component's state and update them via this.setState
, otherwise your component may not see value changes and it will not be refreshed when needed.
While split.js
can split bundles onto separate files and reformat js code it cannot analyze modules' exports. For quick analyzis you may use dev-dumpVivaldi.js
mod which defines vivaldi.jdhooks.dumpVivaldi()
function. The function returns modules' exports sorted by categories and prettified in some cases. For example to check functions working with settings you may find and open elements objects
and vivaldiSettings
inside it.