stylus/edit.js

1146 lines
37 KiB
JavaScript
Raw Normal View History

"use strict";
2015-01-30 17:05:06 +00:00
var styleId = null;
var dirty = {}; // only the actually dirty items here
var editors = []; // array of all CodeMirror instances
var saveSizeOnClose;
var useHistoryBack; // use browser history back when "back to manage" is clicked
2015-01-30 17:05:06 +00:00
// direct & reverse mapping of @-moz-document keywords and internal property names
var propertyToCss = {urls: "url", urlPrefixes: "url-prefix", domains: "domain", regexps: "regexp"};
var CssToProperty = {"url": "urls", "url-prefix": "urlPrefixes", "domain": "domains", "regexp": "regexps"};
// templates
var appliesToTemplate = tHTML('\
<li>\
<select name="applies-type" class="applies-type style-contributor">\
<option value="url" i18n-text="appliesUrlOption"></option>\
<option value="url-prefix" i18n-text="appliesUrlPrefixOption"></option>\
<option value="domain" i18n-text="appliesDomainOption"></option>\
<option value="regexp" i18n-text="appliesRegexpOption"></option>\
</select>\
<input name="applies-value" class="applies-value style-contributor">\
<button class="remove-applies-to" i18n-text="appliesRemove"></button>\
<button class="add-applies-to" i18n-text="appliesAdd"></button>\
</li>\
');
var appliesToEverythingTemplate = tHTML('\
<li class="applies-to-everything" i18n-html="appliesToEverything")>\
<button class="add-applies-to" i18n-text="appliesSpecify"></button>\
</li>\
');
var sectionTemplate = tHTML('\
<div>\
<label i18n-text="sectionCode"></label>\
<textarea class="code"></textarea>\
<br>\
<div class="applies-to">\
<label i18n-text="appliesLabel">\
&nbsp;<img class="applies-to-help" src="help.png" i18n-alt="helpAlt">\
</label>\
<ul class="applies-to-list"></ul>\
</div>\
<button class="remove-section" i18n-text="sectionRemove"></button>\
<button class="add-section" i18n-text="sectionAdd"></button>\
</div>\
');
2015-03-28 08:18:54 +00:00
var findTemplate = t("search") + ': <input type="text" style="width: 10em" class="CodeMirror-search-field"/>&nbsp;' +
'<span style="color: #888" class="CodeMirror-search-hint">(' + t("searchRegexp") + ')</span>';
var jumpToLineTemplate = t('editGotoLine') + ': <input class="CodeMirror-jump-field" type="text" style="width: 5em"/>';
// make querySelectorAll enumeration code readable
["forEach", "some", "indexOf"].forEach(function(method) {
NodeList.prototype[method]= Array.prototype[method];
});
// reroute handling to nearest editor when keypress resolves to one of these commands
var commandsToReroute = {
save: true, jumpToLine: true, nextEditor: true, prevEditor: true,
find: true, findNext: true, findPrev: true, replace: true, replaceAll: true
};
function onChange(event) {
var node = event.target;
if ("savedValue" in node) {
var currentValue = "checkbox" === node.type ? node.checked : node.value;
setCleanItem(node, node.savedValue === currentValue);
} else {
// the manually added section's applies-to is dirty only when the value is non-empty
setCleanItem(node, node.localName != "input" || !node.value.trim());
delete node.savedValue; // only valid when actually saved
}
updateTitle();
}
// Set .dirty on stylesheet contributors that have changed
function setDirtyClass(node, isDirty) {
node.classList.toggle("dirty", isDirty);
}
function setCleanItem(node, isClean) {
if (!node.id) {
node.id = Date.now().toString(32).substr(-6);
}
if (isClean) {
delete dirty[node.id];
2015-04-08 18:24:26 +00:00
// A div would indicate a section
if (node.nodeName.toLowerCase() == "div") {
node.savedValue = getCodeMirrorForSection(node).changeGeneration();
} else {
node.savedValue = "checkbox" === node.type ? node.checked : node.value;
}
} else {
dirty[node.id] = true;
}
setDirtyClass(node, !isClean);
}
function isCleanGlobal() {
var clean = Object.keys(dirty).length == 0;
setDirtyClass(document.body, !clean);
return clean;
}
function setCleanGlobal() {
document.querySelectorAll("#header, #sections > div").forEach(setCleanSection);
dirty = {}; // forget the dirty applies-to ids from a deleted section after the style was saved
}
function setCleanSection(section) {
section.querySelectorAll(".style-contributor").forEach(function(node) { setCleanItem(node, true) });
// #header section has no codemirror
2015-04-08 18:24:26 +00:00
var cm = getCodeMirrorForSection(section)
if (cm) {
section.savedValue = cm.changeGeneration();
indicateCodeChange(cm);
}
}
function initCodeMirror() {
var CM = CodeMirror;
var isWindowsOS = navigator.appVersion.indexOf("Windows") > 0;
// default option values
var userOptions = prefs.getPref("editor.options");
var stylishOptions = {
mode: 'css',
lineNumbers: true,
lineWrapping: true,
foldGutter: true,
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter", "CodeMirror-lint-markers"],
matchBrackets: true,
lint: CodeMirror.lint.css,
2015-05-27 18:01:05 +00:00
styleActiveLine: true,
theme: "default",
keyMap: prefs.getPref("editor.keyMap"),
extraKeys: { // independent of current keyMap
"Alt-PageDown": "nextEditor",
"Alt-PageUp": "prevEditor"
}
}
mergeOptions(stylishOptions, CM.defaults);
mergeOptions(userOptions, CM.defaults);
function mergeOptions(source, target) {
for (var key in source) target[key] = source[key];
return target;
}
// additional commands
CM.commands.jumpToLine = jumpToLine;
CM.commands.nextEditor = function(cm) { nextPrevEditor(cm, 1) };
CM.commands.prevEditor = function(cm) { nextPrevEditor(cm, -1) };
CM.commands.save = save;
CM.commands.blockComment = function(cm) {
cm.blockComment(cm.getCursor("from"), cm.getCursor("to"), {fullLines: false});
};
// "basic" keymap only has basic keys by design, so we skip it
var extraKeysCommands = {};
if (userOptions && typeof userOptions.extraKeys == "object") {
Object.keys(userOptions.extraKeys).forEach(function(key) {
extraKeysCommands[userOptions.extraKeys[key]] = true;
});
}
if (!extraKeysCommands.jumpToLine) {
CM.keyMap.sublime["Ctrl-G"] = "jumpToLine";
CM.keyMap.emacsy["Ctrl-G"] = "jumpToLine";
CM.keyMap.pcDefault["Ctrl-J"] = "jumpToLine";
CM.keyMap.macDefault["Cmd-J"] = "jumpToLine";
}
if (!extraKeysCommands.autocomplete) {
CM.keyMap.pcDefault["Ctrl-Space"] = "autocomplete"; // will be used by "sublime" on PC via fallthrough
CM.keyMap.macDefault["Alt-Space"] = "autocomplete"; // OSX uses Ctrl-Space and Cmd-Space for something else
CM.keyMap.emacsy["Alt-/"] = "autocomplete"; // copied from "emacs" keymap
// "vim" and "emacs" define their own autocomplete hotkeys
}
if (!extraKeysCommands.blockComment) {
CM.keyMap.sublime["Shift-Ctrl-/"] = "blockComment";
}
if (isWindowsOS) {
// "pcDefault" keymap on Windows should have F3/Shift-F3
if (!extraKeysCommands.findNext) {
CM.keyMap.pcDefault["F3"] = "findNext";
}
if (!extraKeysCommands.findPrev) {
CM.keyMap.pcDefault["Shift-F3"] = "findPrev";
}
// try to remap non-interceptable Ctrl-(Shift-)N/T/W hotkeys
["N", "T", "W"].forEach(function(char) {
[{from: "Ctrl-", to: ["Alt-", "Ctrl-Alt-"]},
{from: "Shift-Ctrl-", to: ["Ctrl-Alt-", "Shift-Ctrl-Alt-"]} // Note: modifier order in CM is S-C-A
].forEach(function(remap) {
var oldKey = remap.from + char;
Object.keys(CM.keyMap).forEach(function(keyMapName) {
var keyMap = CM.keyMap[keyMapName];
var command = keyMap[oldKey];
if (!command) {
return;
}
remap.to.some(function(newMod) {
var newKey = newMod + char;
if (!(newKey in keyMap)) {
delete keyMap[oldKey];
keyMap[newKey] = command;
return true;
}
});
});
});
});
}
// user option values
CM.getOption = function (o) {
return CodeMirror.defaults[o];
}
CM.setOption = function (o, v) {
CodeMirror.defaults[o] = v;
editors.forEach(function(editor) {
editor.setOption(o, v);
});
}
// preload the theme so that CodeMirror can calculate its metrics in DOMContentLoaded->loadPrefs()
var theme = prefs.getPref("editor.theme");
document.getElementById("cm-theme").href = theme == "default" ? "" : "codemirror/theme/" + theme + ".css";
// initialize global editor controls
document.addEventListener("DOMContentLoaded", function() {
2015-05-05 20:42:38 +00:00
function optionsHtmlFromArray(options) {
return options.map(function(opt) { return "<option>" + opt + "</option>"; }).join("");
}
var themeControl = document.getElementById("editor.theme");
2015-05-05 20:42:38 +00:00
var bg = chrome.extension.getBackgroundPage();
if (bg && bg.codeMirrorThemes) {
2015-05-05 20:42:38 +00:00
themeControl.innerHTML = optionsHtmlFromArray(bg.codeMirrorThemes);
} else {
// Chrome is starting up and shows our edit.html, but the background page isn't loaded yet
2015-05-05 20:42:38 +00:00
themeControl.innerHTML = optionsHtmlFromArray([theme == "default" ? t("defaultTheme") : theme]);
getCodeMirrorThemes(function(themes) {
2015-05-05 20:42:38 +00:00
themeControl.innerHTML = optionsHtmlFromArray(themes);
themeControl.selectedIndex = Math.max(0, themes.indexOf(theme));
});
}
2015-05-05 20:42:38 +00:00
document.getElementById("editor.keyMap").innerHTML = optionsHtmlFromArray(Object.keys(CM.keyMap).sort());
var controlPrefs = {};
document.querySelectorAll("#options *[data-option][id^='editor.']").forEach(function(option) {
controlPrefs[option.id] = CM.defaults[option.dataset.option];
});
document.getElementById("options").addEventListener("change", acmeEventListener, false);
loadPrefs(controlPrefs);
});
}
initCodeMirror();
function acmeEventListener(event) {
var el = event.target;
var option = el.dataset.option;
//console.log("acmeEventListener heard %s on %s", event.type, el.id);
if (!option) {
console.error("acmeEventListener: no 'cm_option' %O", el);
return;
}
var value = el.type == "checkbox" ? el.checked : el.value;
switch (option) {
case "tabSize":
CodeMirror.setOption("indentUnit", value);
break;
case "theme":
var themeLink = document.getElementById("cm-theme");
// use non-localized "default" internally
2015-05-05 20:42:38 +00:00
if (!value || value == "default" || value == t("defaultTheme")) {
value = "default";
if (prefs.getPref(el.id) != value) {
prefs.setPref(el.id, value);
}
themeLink.href = "";
el.selectedIndex = 0;
break;
}
var url = chrome.extension.getURL("codemirror/theme/" + value + ".css");
if (themeLink.href == url) { // preloaded in initCodeMirror()
break;
}
// avoid flicker: wait for the second stylesheet to load, then apply the theme
document.head.insertAdjacentHTML("beforeend",
'<link id="cm-theme2" rel="stylesheet" href="' + url + '">');
(function() {
setTimeout(function() {
CodeMirror.setOption(option, value);
themeLink.remove();
document.getElementById("cm-theme2").id = "cm-theme";
}, 100);
})();
return;
}
CodeMirror.setOption(option, value);
}
// replace given textarea with the CodeMirror editor
function setupCodeMirror(textarea, index) {
var cm = CodeMirror.fromTextArea(textarea);
cm.on("change", indicateCodeChange);
cm.on("blur", function(cm) { editors.lastActive = cm });
2015-03-25 13:08:28 +00:00
var resizeGrip = cm.display.wrapper.appendChild(document.createElement("div"));
resizeGrip.className = "resize-grip";
resizeGrip.addEventListener("mousedown", function(e) {
e.preventDefault();
var cm = e.target.parentNode.CodeMirror;
var minHeight = cm.defaultTextHeight()
+ cm.display.lineDiv.offsetParent.offsetTop /* .CodeMirror-lines padding */
+ cm.display.wrapper.offsetHeight - cm.display.wrapper.scrollHeight /* borders */;
function resize(e) {
cm.setSize(null, Math.max(minHeight, cm.display.wrapper.scrollHeight + e.movementY));
}
document.addEventListener("mousemove", resize);
document.addEventListener("mouseup", function resizeStop() {
document.removeEventListener("mouseup", resizeStop);
document.removeEventListener("mousemove", resize);
});
});
// resizeGrip has enough space when scrollbars.horiz is visible
if (cm.display.scrollbars.horiz.style.display != "") {
cm.display.scrollbars.vert.style.marginBottom = "0";
}
// resizeGrip space adjustment in case a long line was entered/deleted by a user
new MutationObserver(function(mutations) {
var hScrollbar = mutations[0].target;
var hScrollbarVisible = hScrollbar.style.display != "";
var vScrollbar = hScrollbar.parentNode.CodeMirror.display.scrollbars.vert;
vScrollbar.style.marginBottom = hScrollbarVisible ? "0" : "";
}).observe(cm.display.scrollbars.horiz, {
attributes: true,
attributeFilter: ["style"]
});
2015-03-25 13:08:28 +00:00
editors.splice(index || editors.length, 0, cm);
return cm;
}
function indicateCodeChange(cm) {
2015-04-08 18:24:26 +00:00
var section = getSectionForCodeMirror(cm);
setCleanItem(section, cm.isClean(section.savedValue));
updateTitle();
2015-01-30 17:05:06 +00:00
}
2015-04-08 18:24:26 +00:00
function getSectionForCodeMirror(cm) {
return cm.getTextArea().parentNode;
}
function getCodeMirrorForSection(section) {
// #header section has no codemirror
var wrapper = section.querySelector(".CodeMirror");
if (wrapper) {
return wrapper.CodeMirror;
}
return null;
}
// prevent the browser from seeing hotkeys that should be handled by nearest editor
document.addEventListener("keydown", function(event) {
if (event.target.localName == "textarea") {
return; // let CodeMirror handle it
}
var keyName = CodeMirror.keyName(event);
if ("handled" == CodeMirror.lookupKey(keyName, CodeMirror.getOption("keyMap"), handleCommand)
|| "handled" == CodeMirror.lookupKey(keyName, CodeMirror.defaults.extraKeys, handleCommand)) {
event.preventDefault();
event.stopPropagation();
}
function handleCommand(command) {
if (commandsToReroute[command] === true) {
CodeMirror.commands[command](getEditorInSight(event.target));
return true;
}
}
});
// remind Chrome to repaint a previously invisible editor box by toggling any element's transform
// this bug is present in some versions of Chrome (v37-40 or something)
document.addEventListener("scroll", function(event) {
var style = document.getElementById("name").style;
style.webkitTransform = style.webkitTransform ? "" : "scale(1)";
});
// Shift-Ctrl-Wheel scrolls entire page even when mouse is over a code editor
document.addEventListener("wheel", function(event) {
if (event.shiftKey && event.ctrlKey && !event.altKey && !event.metaKey) {
// Chrome scrolls horizontally when Shift is pressed but on some PCs this might be different
window.scrollBy(0, event.deltaX || event.deltaY);
event.preventDefault();
}
});
chrome.tabs.query({currentWindow: true}, function(tabs) {
var windowId = tabs[0].windowId;
if (prefs.getPref("openEditInWindow")) {
if (tabs.length == 1 && window.history.length == 1) {
chrome.windows.getAll(function(windows) {
if (windows.length > 1) {
sessionStorageHash("saveSizeOnClose").set(windowId, true);
saveSizeOnClose = true;
}
});
} else {
saveSizeOnClose = sessionStorageHash("saveSizeOnClose").value[windowId];
}
}
chrome.tabs.onRemoved.addListener(function(tabId, info) {
sessionStorageHash("manageStylesHistory").unset(tabId);
if (info.windowId == windowId && info.isWindowClosing) {
sessionStorageHash("saveSizeOnClose").unset(windowId);
}
});
});
getActiveTab(function(tab) {
useHistoryBack = sessionStorageHash("manageStylesHistory").value[tab.id] == location.href;
});
function goBackToManage(event) {
if (useHistoryBack) {
event.stopPropagation();
event.preventDefault();
history.back();
}
}
2015-01-30 17:05:06 +00:00
window.onbeforeunload = function() {
if (saveSizeOnClose) {
prefs.setPref("windowPosition", {
left: screenLeft,
top: screenTop,
width: outerWidth,
height: outerHeight
});
}
document.activeElement.blur();
return !isCleanGlobal() ? t('styleChangesNotSaved') : null;
}
2015-01-30 17:05:06 +00:00
function addAppliesTo(list, name, value) {
var showingEverything = list.querySelector(".applies-to-everything") != null;
// blow away "Everything" if it's there
if (showingEverything) {
list.removeChild(list.firstChild);
}
var e;
if (name && value) {
e = appliesToTemplate.cloneNode(true);
e.querySelector("[name=applies-type]").value = name;
e.querySelector("[name=applies-value]").value = value;
e.querySelector(".remove-applies-to").addEventListener("click", removeAppliesTo, false);
} else if (showingEverything || list.hasChildNodes()) {
e = appliesToTemplate.cloneNode(true);
if (list.hasChildNodes()) {
e.querySelector("[name=applies-type]").value = list.querySelector("li:last-child [name='applies-type']").value;
}
e.querySelector(".remove-applies-to").addEventListener("click", removeAppliesTo, false);
} else {
e = appliesToEverythingTemplate.cloneNode(true);
}
e.querySelector(".add-applies-to").addEventListener("click", function() {addAppliesTo(this.parentNode.parentNode)}, false);
list.appendChild(e);
}
function addSection(event, section) {
2015-01-30 17:05:06 +00:00
var div = sectionTemplate.cloneNode(true);
div.querySelector(".applies-to-help").addEventListener("click", showAppliesToHelp, false);
div.querySelector(".remove-section").addEventListener("click", removeSection, false);
div.querySelector(".add-section").addEventListener("click", addSection, false);
2015-01-30 17:05:06 +00:00
var codeElement = div.querySelector(".code");
2015-01-30 17:05:06 +00:00
var appliesTo = div.querySelector(".applies-to-list");
var appliesToAdded = false;
2015-01-30 17:05:06 +00:00
if (section) {
codeElement.value = section.code;
for (var i in propertyToCss) {
if (section[i]) {
section[i].forEach(function(url) {
addAppliesTo(appliesTo, propertyToCss[i], url);
appliesToAdded = true;
});
}
2015-01-30 17:05:06 +00:00
}
}
if (!appliesToAdded) {
2015-01-30 17:05:06 +00:00
addAppliesTo(appliesTo);
}
appliesTo.addEventListener("change", onChange);
appliesTo.addEventListener("input", onChange);
2015-01-30 17:05:06 +00:00
var sections = document.getElementById("sections");
if (event) {
var clickedSection = event.target.parentNode;
sections.insertBefore(div, clickedSection.nextElementSibling);
var newIndex = document.querySelectorAll("#sections > div").indexOf(clickedSection) + 1;
var cm = setupCodeMirror(codeElement, newIndex);
makeSectionVisible(cm);
cm.focus()
} else {
sections.appendChild(div);
setupCodeMirror(codeElement);
}
setCleanSection(div);
return div;
2015-01-30 17:05:06 +00:00
}
function removeAppliesTo(event) {
var appliesTo = event.target.parentNode,
appliesToList = appliesTo.parentNode;
removeAreaAndSetDirty(appliesTo);
2015-01-30 17:05:06 +00:00
if (!appliesToList.hasChildNodes()) {
addAppliesTo(appliesToList);
2015-01-30 17:05:06 +00:00
}
}
function removeSection(event) {
var section = event.target.parentNode;
var cm = section.querySelector(".CodeMirror").CodeMirror;
removeAreaAndSetDirty(section);
editors.splice(editors.indexOf(cm), 1);
}
function removeAreaAndSetDirty(area) {
area.querySelectorAll('.style-contributor').some(function(node) {
if (node.savedValue) {
// it's a saved section, so make it dirty and stop the enumeration
setCleanItem(area, false);
return true;
} else {
// it's an empty section, so undirty the applies-to items,
// otherwise orphaned ids would keep the style dirty
setCleanItem(node, true);
}
});
updateTitle();
area.parentNode.removeChild(area);
2015-01-30 17:05:06 +00:00
}
function makeSectionVisible(cm) {
var section = getSectionForCodeMirror(cm);
var bounds = section.getBoundingClientRect();
if ((bounds.bottom > window.innerHeight && bounds.top > 0) || (bounds.top < 0 && bounds.bottom < window.innerHeight)) {
if (bounds.top < 0) {
window.scrollBy(0, bounds.top - 1);
} else {
window.scrollBy(0, bounds.bottom - window.innerHeight + 1);
}
}
}
function setupGlobalSearch() {
var originalCommand = {
find: CodeMirror.commands.find,
findNext: CodeMirror.commands.findNext,
findPrev: CodeMirror.commands.findPrev
}
var curState; // cm.state.search for last used 'find'
function shouldIgnoreCase(query) { // treat all-lowercase non-regexp queries as case-insensitive
return typeof query == "string" && query == query.toLowerCase();
}
function updateState(cm, newState) {
if (!newState) {
if (cm.state.search) {
return cm.state.search;
}
newState = curState;
}
cm.state.search = {
query: newState.query,
overlay: newState.overlay,
annotate: cm.showMatchesOnScrollbar(newState.query, shouldIgnoreCase(newState.query))
}
cm.addOverlay(newState.overlay);
return cm.state.search;
}
function find(activeCM) {
editors.lastActive = activeCM;
var cm = getEditorInSight();
if (cm != activeCM) {
cm.focus();
activeCM = cm;
}
var originalOpenDialog = activeCM.openDialog;
activeCM.openDialog = function(template, callback, options) {
2015-03-28 08:18:54 +00:00
originalOpenDialog.call(activeCM, findTemplate, function(query) {
activeCM.openDialog = originalOpenDialog;
callback(query);
curState = activeCM.state.search;
if (editors.length == 1 || !curState.query) {
return;
}
editors.forEach(function(cm) {
if (cm != activeCM) {
cm.execCommand("clearSearch");
updateState(cm, curState);
}
});
if (CodeMirror.cmpPos(curState.posFrom, curState.posTo) == 0) {
findNext(activeCM);
}
}, options);
}
originalCommand.find(activeCM);
}
function findNext(activeCM, reverse) {
var state = updateState(activeCM);
if (!state || !state.query) {
find(activeCM);
return;
}
var pos = activeCM.getCursor(reverse ? "from" : "to");
activeCM.setSelection(activeCM.getCursor()); // clear the selection, don't move the cursor
var rxQuery = typeof state.query == "object"
? state.query : stringAsRegExp(state.query, shouldIgnoreCase(state.query) ? "i" : "");
if (document.activeElement && document.activeElement.name == "applies-value"
&& searchAppliesTo(activeCM)) {
return;
}
for (var i=0, cm=activeCM; i < editors.length; i++) {
state = updateState(cm);
if (!cm.hasFocus()) {
pos = reverse ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(0, 0);
}
var searchCursor = cm.getSearchCursor(state.query, pos, shouldIgnoreCase(state.query));
if (searchCursor.find(reverse)) {
if (editors.length > 1) {
makeSectionVisible(cm);
cm.focus();
}
// speedup the original findNext
state.posFrom = reverse ? searchCursor.to() : searchCursor.from();
state.posTo = CodeMirror.Pos(state.posFrom.line, state.posFrom.ch);
originalCommand[reverse ? "findPrev" : "findNext"](cm);
return;
} else if (!reverse && searchAppliesTo(cm)) {
return;
}
cm = editors[(editors.indexOf(cm) + (reverse ? -1 + editors.length : 1)) % editors.length];
if (reverse && searchAppliesTo(cm)) {
return;
}
}
// nothing found so far, so call the original search with wrap-around
originalCommand[reverse ? "findPrev" : "findNext"](activeCM);
function searchAppliesTo(cm) {
var inputs = [].slice.call(getSectionForCodeMirror(cm).querySelectorAll(".applies-value"));
if (reverse) {
inputs = inputs.reverse();
}
inputs.splice(0, inputs.indexOf(document.activeElement) + 1);
return inputs.some(function(input) {
var match = rxQuery.exec(input.value);
if (match) {
input.focus();
var end = match.index + match[0].length;
// scroll selected part into view in long inputs,
// works only outside of current event handlers chain, hence timeout=0
setTimeout(function() {
input.setSelectionRange(end, end);
input.setSelectionRange(match.index, end)
}, 0);
return true;
}
});
}
}
function findPrev(cm) {
findNext(cm, true);
}
CodeMirror.commands.find = find;
CodeMirror.commands.findNext = findNext;
CodeMirror.commands.findPrev = findPrev;
}
function jumpToLine(cm) {
var cur = cm.getCursor();
cm.openDialog(jumpToLineTemplate, function(str) {
var m = str.match(/^\s*(\d+)(?:\s*:\s*(\d+))?\s*$/);
if (m) {
cm.setCursor(m[1] - 1, m[2] ? m[2] - 1 : cur.ch);
}
}, {value: cur.line+1});
}
function nextPrevEditor(cm, direction) {
cm = editors[(editors.indexOf(cm) + direction + editors.length) % editors.length];
makeSectionVisible(cm);
cm.focus();
}
function getEditorInSight(nearbyElement) {
// priority: 1. associated CM for applies-to element 2. last active if visible 3. first visible
var cm;
if (nearbyElement && nearbyElement.className.indexOf("applies-") >= 0) {
cm = getCodeMirrorForSection(querySelectorParent(nearbyElement, "#sections > div"));
} else {
cm = editors.lastActive;
}
if (!cm || offscreenDistance(cm) > 0) {
var sorted = editors
.map(function(cm, index) { return {cm: cm, distance: offscreenDistance(cm), index: index} })
.sort(function(a, b) { return a.distance - b.distance || a.index - b.index });
cm = sorted[0].cm;
if (sorted[0].distance > 0) {
makeSectionVisible(cm)
}
}
return cm;
function offscreenDistance(cm) {
var LINES_VISIBLE = 2; // closest editor should have at least # lines visible
var bounds = getSectionForCodeMirror(cm).getBoundingClientRect();
if (bounds.top < 0) {
return -bounds.top;
} else if (bounds.top < window.innerHeight - cm.defaultTextHeight() * LINES_VISIBLE) {
return 0;
} else {
return bounds.top - bounds.height;
}
}
}
2015-01-30 17:05:06 +00:00
window.addEventListener("load", init, false);
function init() {
var params = getParams();
if (!params.id) { // match should be 2 - one for the whole thing, one for the parentheses
2015-01-30 17:05:06 +00:00
// This is an add
var section = {code: ""}
for (var i in CssToProperty) {
if (params[i]) {
section[CssToProperty[i]] = [params[i]];
}
}
addSection(null, section);
2015-01-30 17:05:06 +00:00
// default to enabled
document.getElementById("enabled").checked = true
tE("heading", "addStyleTitle");
initHooks();
2015-01-30 17:05:06 +00:00
return;
}
// This is an edit
requestStyle();
function requestStyle() {
chrome.extension.sendMessage({method: "getStyles", id: params.id}, function callback(styles) {
if (!styles) { // Chrome is starting up and shows edit.html
requestStyle();
return;
}
var style = styles[0];
styleId = style.id;
initWithStyle(style);
});
}
2015-01-30 17:05:06 +00:00
}
function initWithStyle(style) {
document.getElementById("name").value = style.name;
document.getElementById("enabled").checked = style.enabled == "true";
document.getElementById("url").href = style.url;
tE("heading", "editStyleHeading", null, false);
2015-01-30 17:05:06 +00:00
// if this was done in response to an update, we need to clear existing sections
document.querySelectorAll("#sections > div").forEach(function(div) {
2015-01-30 17:05:06 +00:00
div.parentNode.removeChild(div);
});
(style.sections.length == 0 ? [{code: ""}] : style.sections).forEach(function(section) {
setTimeout(function() {
maximizeCodeHeight(addSection(null, section), editors.length == style.sections.length);
}, 0);
});
initHooks();
}
function initHooks() {
document.querySelectorAll("#header .style-contributor").forEach(function(node) {
node.addEventListener("change", onChange);
node.addEventListener("input", onChange);
});
document.getElementById("to-mozilla").addEventListener("click", showMozillaFormat, false);
document.getElementById("to-mozilla-help").addEventListener("click", showToMozillaHelp, false);
document.getElementById("save-button").addEventListener("click", save, false);
document.getElementById("sections-help").addEventListener("click", showSectionHelp, false);
document.getElementById("keyMap-help").addEventListener("click", showKeyMapHelp, false);
document.getElementById("cancel-button").addEventListener("click", goBackToManage);
setupGlobalSearch();
setCleanGlobal();
updateTitle();
2015-01-30 17:05:06 +00:00
}
function maximizeCodeHeight(sectionDiv, isLast) {
var cm = getCodeMirrorForSection(sectionDiv);
var stats = maximizeCodeHeight.stats = maximizeCodeHeight.stats || {totalHeight: 0, deltas: []};
if (!stats.cmActualHeight) {
stats.cmActualHeight = getComputedHeight(cm.display.wrapper);
}
if (!stats.sectionMarginTop) {
stats.sectionMarginTop = parseFloat(getComputedStyle(sectionDiv).marginTop);
}
var sectionTop = sectionDiv.getBoundingClientRect().top - stats.sectionMarginTop;
if (!stats.firstSectionTop) {
stats.firstSectionTop = sectionTop;
}
var extrasHeight = getComputedHeight(sectionDiv) - stats.cmActualHeight;
var cmMaxHeight = window.innerHeight - extrasHeight - sectionTop - stats.sectionMarginTop;
var cmDesiredHeight = cm.display.sizer.clientHeight + 2*cm.defaultTextHeight();
var cmGrantableHeight = Math.max(stats.cmActualHeight, Math.min(cmMaxHeight, cmDesiredHeight));
stats.deltas.push(cmGrantableHeight - stats.cmActualHeight);
stats.totalHeight += cmGrantableHeight + extrasHeight;
if (!isLast) {
return;
}
stats.totalHeight += stats.firstSectionTop;
if (stats.totalHeight <= window.innerHeight) {
editors.forEach(function(cm, index) {
cm.setSize(null, stats.deltas[index] + stats.cmActualHeight);
});
return;
}
// scale heights to fill the gap between last section and bottom edge of the window
var sections = document.getElementById("sections");
var available = window.innerHeight - sections.getBoundingClientRect().bottom -
parseFloat(getComputedStyle(sections).marginBottom);
if (available <= 0) {
return;
}
var totalDelta = stats.deltas.reduce(function(sum, d) { return sum + d; }, 0);
var q = available / totalDelta;
var baseHeight = stats.cmActualHeight - stats.sectionMarginTop;
stats.deltas.forEach(function(delta, index) {
editors[index].setSize(null, baseHeight + Math.floor(q * delta));
});
}
function updateTitle() {
var DIRTY_TITLE = "* $";
var name = document.getElementById("name").savedValue;
var clean = isCleanGlobal();
var title = styleId === null ? t("addStyleTitle") : t('editStyleTitle', [name]);
document.title = clean ? title : DIRTY_TITLE.replace("$", title);
2015-01-30 17:05:06 +00:00
}
function validate() {
var name = document.getElementById("name").value;
if (name == "") {
return t("styleMissingName");
}
// validate the regexps
if (document.querySelectorAll(".applies-to-list").some(function(list) {
return list.childNodes.some(function(li) {
2015-01-30 17:05:06 +00:00
if (li.className == appliesToEverythingTemplate.className) {
return false;
}
var valueElement = li.querySelector("[name=applies-value]");
var type = li.querySelector("[name=applies-type]").value;
var value = valueElement.value;
if (type && value) {
if (type == "regexp") {
2015-01-30 17:05:06 +00:00
try {
new RegExp(value);
2015-01-30 17:05:06 +00:00
} catch (ex) {
valueElement.focus();
return true;
}
}
}
return false;
});
})) {
return t("styleBadRegexp");
}
return null;
}
function save() {
// save the contents of the CodeMirror editors back into the textareas
for (var i=0; i < editors.length; i++) {
editors[i].save();
}
2015-01-30 17:05:06 +00:00
var error = validate();
if (error) {
alert(error);
return;
}
var name = document.getElementById("name").value;
var enabled = document.getElementById("enabled").checked;
var request = {
method: "saveStyle",
id: styleId,
name: name,
enabled: enabled,
sections: getSections()
2015-01-30 17:05:06 +00:00
};
chrome.extension.sendMessage(request, saveComplete);
}
function getSections() {
var sections = [];
document.querySelectorAll("#sections > div").forEach(function(div) {
var meta = getMeta(div);
var code = div.querySelector(".CodeMirror").CodeMirror.getValue();
if (/^\s*$/.test(code) && Object.keys(meta).length == 0) {
2015-01-30 17:05:06 +00:00
return;
}
meta.code = code;
sections.push(meta);
});
return sections;
}
function getMeta(e) {
var meta = {};
e.querySelector(".applies-to-list").childNodes.forEach(function(li) {
2015-01-30 17:05:06 +00:00
if (li.className == appliesToEverythingTemplate.className) {
return;
}
var type = li.querySelector("[name=applies-type]").value;
var value = li.querySelector("[name=applies-value]").value;
if (type && value) {
var property = CssToProperty[type];
meta[property] ? meta[property].push(value) : meta[property] = [value];
2015-01-30 17:05:06 +00:00
}
});
return meta;
}
function saveComplete(style) {
styleId = style.id;
setCleanGlobal();
2015-01-30 17:05:06 +00:00
// Go from new style URL to edit style URL
if (location.href.indexOf("id=") == -1) {
history.replaceState({}, document.title, "edit.html?id=" + style.id);
2015-01-30 17:05:06 +00:00
}
updateTitle();
2015-01-30 17:05:06 +00:00
}
function showMozillaFormat() {
window.open("data:text/plain;charset=UTF-8," + encodeURIComponent(toMozillaFormat()));
2015-01-30 17:05:06 +00:00
}
function toMozillaFormat() {
return getSections().map(function(section) {
var cssMds = [];
for (var i in propertyToCss) {
if (section[i]) {
cssMds = cssMds.concat(section[i].map(function (v){
return propertyToCss[i] + "(\"" + v.replace(/\\/g, "\\\\") + "\")";
}));
}
2015-01-30 17:05:06 +00:00
}
return cssMds.length ? "@-moz-document " + cssMds.join(", ") + " {\n" + section.code + "\n}" : section.code;
2015-01-30 17:05:06 +00:00
}).join("\n\n");
}
function showSectionHelp() {
showHelp(t("styleSectionsTitle"), t("sectionHelp"));
2015-01-30 17:05:06 +00:00
}
function showAppliesToHelp() {
showHelp(t("appliesLabel"), t("appliesHelp"));
2015-01-30 17:05:06 +00:00
}
function showToMozillaHelp() {
showHelp(t("styleToMozillaFormat"), t("styleToMozillaFormatHelp"));
}
function showKeyMapHelp() {
var keyMap = mergeKeyMaps({}, prefs.getPref("editor.keyMap"), CodeMirror.defaults.extraKeys);
var keyMapSorted = Object.keys(keyMap)
.map(function(key) { return {key: key, cmd: keyMap[key]} })
.concat([{key: "Shift-Ctrl-Wheel", cmd: "scrollWindow"}])
.sort(function(a, b) { return a.cmd < b.cmd || (a.cmd == b.cmd && a.key < b.key) ? -1 : 1 });
showHelp(t("cm_keyMap") + ": " + prefs.getPref("editor.keyMap"),
'<table class="keymap-list">' +
'<thead><tr><th><input placeholder="' + t("helpKeyMapHotkey") + '" type="search"></th>' +
'<th><input placeholder="' + t("helpKeyMapCommand") + '" type="search"></th></tr></thead>' +
"<tbody>" + keyMapSorted.map(function(value) {
return "<tr><td>" + value.key + "</td><td>" + value.cmd + "</td></tr>";
}).join("") +
"</tbody>" +
"</table>");
var table = document.querySelector("#help-popup table");
table.addEventListener("input", filterTable);
var inputs = table.querySelectorAll("input");
inputs[0].addEventListener("keydown", hotkeyHandler);
inputs[1].focus();
function hotkeyHandler(event) {
var keyName = CodeMirror.keyName(event);
if (keyName == "Esc" || keyName == "Tab" || keyName == "Shift-Tab") {
return;
}
event.preventDefault();
event.stopPropagation();
// normalize order of modifiers,
// for modifier-only keys ("Ctrl-Shift") a dummy main key has to be temporarily added
var keyMap = {};
keyMap[keyName.replace(/(Shift|Ctrl|Alt|Cmd)$/, "$&-dummy")] = "";
var normalizedKey = Object.keys(CodeMirror.normalizeKeyMap(keyMap))[0];
this.value = normalizedKey.replace("-dummy", "");
filterTable(event);
}
function filterTable(event) {
var input = event.target;
var query = stringAsRegExp(input.value, "gi");
var col = input.parentNode.cellIndex;
inputs[1 - col].value = "";
table.tBodies[0].childNodes.forEach(function(row) {
var cell = row.children[col];
cell.innerHTML = cell.textContent.replace(query, "<mark>$&</mark>");
row.style.display = query.test(cell.textContent) ? "" : "none";
// clear highlight from the other column
cell = row.children[1 - col];
cell.innerHTML = cell.textContent;
});
}
function mergeKeyMaps(merged) {
[].slice.call(arguments, 1).forEach(function(keyMap) {
if (typeof keyMap == "string") {
keyMap = CodeMirror.keyMap[keyMap];
}
Object.keys(keyMap).forEach(function(key) {
var cmd = keyMap[key];
// filter out '...', 'attach', etc. (hotkeys start with an uppercase letter)
if (!merged[key] && !key.match(/^[a-z]/) && cmd != "...") {
if (typeof cmd == "function") {
// for 'emacs' keymap: provide at least something meaningful (hotkeys and the function body)
// for 'vim*' keymaps: almost nothing as it doesn't rely on CM keymap mechanism
cmd = cmd.toString().replace(/^function.*?\{[\s\r\n]*([\s\S]+?)[\s\r\n]*\}$/, "$1");
merged[key] = cmd.length <= 200 ? cmd : cmd.substr(0, 200) + "...";
} else {
merged[key] = cmd;
}
}
});
if (keyMap.fallthrough) {
merged = mergeKeyMaps(merged, keyMap.fallthrough);
}
});
return merged;
}
2015-01-30 17:05:06 +00:00
}
function showHelp(title, text) {
var div = document.getElementById("help-popup");
div.querySelector(".contents").innerHTML = text;
div.querySelector(".title").innerHTML = title;
if (getComputedStyle(div).display == "none") {
document.addEventListener("keydown", closeHelp);
div.querySelector(".close-icon").onclick = closeHelp; // avoid chaining on multiple showHelp() calls
}
div.style.display = "block";
function closeHelp(e) {
if (e.type == "click" || (e.keyCode == 27 && !e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey)) {
div.style.display = "";
document.removeEventListener("keydown", closeHelp);
}
}
2015-01-30 17:05:06 +00:00
}
function getParams() {
var params = {};
var urlParts = location.href.split("?", 2);
if (urlParts.length == 1) {
return params;
}
urlParts[1].split("&").forEach(function(keyValue) {
var splitKeyValue = keyValue.split("=", 2);
params[decodeURIComponent(splitKeyValue[0])] = decodeURIComponent(splitKeyValue[1]);
});
return params;
}
2015-01-30 17:05:06 +00:00
chrome.extension.onMessage.addListener(function(request, sender, sendResponse) {
switch (request.method) {
2015-01-30 17:05:06 +00:00
case "styleUpdated":
if (styleId == request.id) {
initWithStyle(request.style);
}
break;
case "styleDeleted":
if (styleId == request.id) {
window.close();
break;
}
break;
case "prefChanged":
if (request.prefName == "editor.smartIndent") {
CodeMirror.setOption("smartIndent", request.value);
}
2015-01-30 17:05:06 +00:00
}
});
function querySelectorParent(node, selector) {
var parent = node.parentNode;
while (parent && parent.matches && !parent.matches(selector))
parent = parent.parentNode;
return parent.matches ? parent : null; // null for the root document.DOCUMENT_NODE
}
function stringAsRegExp(s, flags) {
return new RegExp(s.replace(/[{}()\[\]\/\\.+?^$:=*!|]/g, "\\$&"), flags);
}
function getComputedHeight(el) {
var compStyle = getComputedStyle(el);
return el.getBoundingClientRect().height +
parseFloat(compStyle.marginTop) + parseFloat(compStyle.marginBottom);
}