<commit_before>import { string } from "lively.lang";
import { Color, pr } from "lively.graphics";
import { Morph, show } from "./index.js";

export class Text extends Morph {

  static makeLabel(string, props) {
    return new Text({
      textString: string,
      fontFamily: "Helvetica Neue, Arial",
      fontColor: Color.black,
      fontSize: 11,
      readOnly: true,
      ...props
    });
  }

  constructor(props) {
    super({
      readOnly: false,
      clipMode: "hidden",
      textString: "",
      fixedWidth: false, fixedHeight: false,
      draggable: false,
      _selection: { start: 0, end: 0 },
      fontFamily: "Sans-Serif",
      fontSize: 12,
      ...props
    });
    this.fit();
    this._needsFit = false;
    this._needsSelect = false;
  }

  get fontMetric() { return this.env.fontMetric; }

  get isText() { return true }

  get textString() { return this.getProperty("textString") }
  set textString(value) {
    let oldText = this.textString;
    oldText && this.deleteText(0, oldText.length);
    this.insertText(0, value);
  }

  get readOnly() { return this.getProperty("readOnly"); }
  set readOnly(value) {
    this.nativeCursor = value ? "default" : "auto";
    this.recordChange({prop: "readOnly", value});
  }

  get fixedWidth() { return this.getProperty("fixedWidth") }
  set fixedWidth(value) {
    this.recordChange({prop: "fixedWidth", value});
    this._needsFit = true;
  }

  get fixedHeight() { return this.getProperty("fixedHeight") }
  set fixedHeight(value) {
    this.recordChange({prop: "fixedHeight", value});
    this._needsFit = true;
  }

  get fontFamily() { return this.getProperty("fontFamily") }
  set fontFamily(value) {
    this.recordChange({prop: "fontFamily", value});
    this._needsFit = true;
  }

  get fontSize() { return this.getProperty("fontSize") }
  set fontSize(value) {
    this.recordChange({prop: "fontSize", value});
    this._needsFit = true;
  }

  get fontColor() { return this.getProperty("fontColor") }
  set fontColor(value) {
    this.recordChange({prop: "fontColor", value});
  }

  get placeholder() { return this.getProperty("placeholder") }
  set placeholder(value) {
    this.recordChange({prop: "placeholder", value});
    this._needsFit = true;
  }

  get _selection() { return this.getProperty("_selection") }
  set _selection(value) { this.recordChange({prop: "_selection", value}); }

  get selection() { return new TextSelection(this) }

  insertText(pos, str) {
    var oldText = this.textString,
        newText = oldText ? oldText.substr(0, pos) + str + oldText.substr(pos) : str;
    this._needsFit = true;

    this.recordChange({
      prop: "textString", value: newText,
      type: "insert",
      pos: pos,
      str: str
    });
  }

  deleteText(start, end) {
    var oldText = this.textString,
        newText = oldText.substr(0, start) + oldText.substr(end);
    this._needsFit = true;

    this.recordChange({
      prop: "textString", value: newText,
      type: "delete",
      start: start,
      end: end
    });
  }

  selectionOrLineString() {
    var sel = this.selection;
    if (sel.text) return sel.text;
    var line = string.lineIndexComputer(this.textString)(sel.start),
        [start, end] = string.lineNumberToIndexesComputer(this.textString)(line);
    return this.textString.slice(start, end).trim();
  }

  aboutToRender() {
    super.aboutToRender();
    this.fitIfNeeded();
  }

  render(renderer) {
    return renderer.renderText(this);
  }

  fit() {
    var {fixedHeight, fixedWidth} = this;
    if (fixedHeight && fixedWidth) return;

    var {fontMetric, fontFamily, fontSize, placeholder, textString} = this,
        {height: placeholderHeight, width: placeholderWidth} = fontMetric.sizeForStr(fontFamily, fontSize, placeholder || " "),
        {height, width} = fontMetric.sizeForStr(fontFamily, fontSize, textString);
    if (!fixedHeight)
      this.height = Math.max(placeholderHeight, height);
    if (!fixedWidth)
      this.width = Math.max(placeholderWidth, width);
  }

  fitIfNeeded() {
    if (this._needsFit) { this.fit(); this._needsFit = false; }
  }

  recordSelectionFrom(domNode) {
    var { selectionStart: start, selectionEnd: end } = domNode;
    this._selection = { start: start, end: end };
  }

  onMouseDown(evt) { this.onMouseMove(evt); }

  onMouseMove(evt) {
    var { clickedOnMorph, clickedOnPosition } = evt.state;
    if (clickedOnMorph === this) {
      var startPos = this.localize(clickedOnPosition),
          endPos = this.localize(evt.position),
          { fontFamily, fontSize, textString, fontMetric, selection } = this,
          { start: curStart, end: curEnd } = selection,
          start = fontMetric.indexFromPoint(fontFamily, fontSize, textString, startPos),
          end = fontMetric.indexFromPoint(fontFamily, fontSize, textString, endPos);
      if (start > end)
        [start, end] = [end, start];
      if (end !== curEnd || start !== curStart)
        selection.range = { start: start, end: end };
    }
  }

  onKeyUp(evt) {
    switch (evt.keyString()) {
      case 'Command-D': case 'Command-P': evt.stop(); break;
    }
  }

  async onKeyDown(evt) {
    var keyString = evt.keyString(),
        key = evt.domEvt.key,
        sel = this.selection;
    switch (keyString) {
      case 'Command-C': case 'Command-X': case 'Command-V':
        break; // handled by onCut()/onPaste()

      case 'Command-D':
        evt.stop();
        var result = await lively.vm.runEval(this.selectionOrLineString(), {System, targetModule: "lively://test-text/1"});
        this.world()[result.isError ? "logError" : "setStatusMessage"](result.value);
        break;

      case 'Command-P':
        evt.stop();
        var result = await lively.vm.runEval(this.selectionOrLineString(), {System, targetModule: "lively://test-text/1"});
        this.textString = this.textString.slice(0, sel.end) + result.value + this.textString.slice(sel.end);
        break;

      case 'Backspace':
        if (this.readOnly) break;
        evt.stop();
        sel.isCollapsed && sel.start && sel.start--;
        sel.text = "";
        sel.collapse();
        break;

      case 'Left': case 'Right':
        evt.stop();
        sel.start += (keyString === 'Right' ? 1 : (sel.start > 0 ? -1 : 0));
        sel.collapse();
        break;

      case 'Up': case 'Down':
        evt.stop();
        var text = this.textString,
            line = string.lineIndexComputer(text)(sel.start),
            otherLine = line + (keyString === "Down" ? 1 : -1),
            rangeComp = string.lineNumberToIndexesComputer(text),
            [lineStart, lineEnd] = rangeComp(line),
            otherLineRange = rangeComp(otherLine),
            [otherLineStart, otherLineEnd] = otherLineRange || [lineStart, lineEnd];
        sel.start = Math.min(otherLineStart + (sel.start - lineStart), otherLineEnd-1);
        sel.collapse();
        break;

      default:
        if (this.readOnly) break;
        // FIXME!
        evt.stop();
        if (key.length !== 1 && key !== "Space" && key !== "Enter") {
          break;
        }
        sel.text = (key === "Enter" ? "\n" : key);
        sel.collapse(sel.start + 1);
    }
  }

  onCut(evt) {
    // FIXME!
    var sel = this.selection;
    sel.text = "";
    sel.collapse();
  }

  onCopy(evt) {
    // TODO!
  }

  onPaste(evt) {
    var sel = this.selection;
    sel.text = evt.domEvt.clipboardData.getData("Text");
    sel.collapse(sel.end);
  }
}

class TextSelection {

  constructor(textMorph) {
    this.textMorph = textMorph;
  }

  get range() { return this.textMorph._selection; }
  set range(rangeObj) {
    let morph = this.textMorph;
    morph._selection = rangeObj;
    morph._needsSelect = true;
  }

  get start() { return this.range.start; }
  set start(val) { this.range = { start: val, end: this.end }; }

  get end() { return this.range.end }
  set end(val) { this.range = { start: this.start, end: val }; }

  get text() { return this.textMorph.textString.substring(this.start, this.end) }
  set text(val) {
    let { start, end } = this.range,
        morph = this.textMorph;
    if (!this.isCollapsed) {
      morph.deleteText(start, end);
    }
    if (val.length) {
      morph.insertText(start, val);
    }
    this.range = { start: this.start, end: this.start + val.length };
  }

  get isCollapsed() { return this.start === this.end; }
  collapse(index = this.start) { this.range = { start: index, end: index }; }
}
<commit_msg>implement onCopy; fix onCut<commit_after>import { string } from "lively.lang";
import { Color, pr } from "lively.graphics";
import { Morph, show } from "./index.js";

export class Text extends Morph {

  static makeLabel(string, props) {
    return new Text({
      textString: string,
      fontFamily: "Helvetica Neue, Arial",
      fontColor: Color.black,
      fontSize: 11,
      readOnly: true,
      ...props
    });
  }

  constructor(props) {
    super({
      readOnly: false,
      clipMode: "hidden",
      textString: "",
      fixedWidth: false, fixedHeight: false,
      draggable: false,
      _selection: { start: 0, end: 0 },
      fontFamily: "Sans-Serif",
      fontSize: 12,
      ...props
    });
    this.fit();
    this._needsFit = false;
    this._needsSelect = false;
  }

  get fontMetric() { return this.env.fontMetric; }

  get isText() { return true }

  get textString() { return this.getProperty("textString") }
  set textString(value) {
    let oldText = this.textString;
    oldText && this.deleteText(0, oldText.length);
    this.insertText(0, value);
  }

  get readOnly() { return this.getProperty("readOnly"); }
  set readOnly(value) {
    this.nativeCursor = value ? "default" : "auto";
    this.recordChange({prop: "readOnly", value});
  }

  get fixedWidth() { return this.getProperty("fixedWidth") }
  set fixedWidth(value) {
    this.recordChange({prop: "fixedWidth", value});
    this._needsFit = true;
  }

  get fixedHeight() { return this.getProperty("fixedHeight") }
  set fixedHeight(value) {
    this.recordChange({prop: "fixedHeight", value});
    this._needsFit = true;
  }

  get fontFamily() { return this.getProperty("fontFamily") }
  set fontFamily(value) {
    this.recordChange({prop: "fontFamily", value});
    this._needsFit = true;
  }

  get fontSize() { return this.getProperty("fontSize") }
  set fontSize(value) {
    this.recordChange({prop: "fontSize", value});
    this._needsFit = true;
  }

  get fontColor() { return this.getProperty("fontColor") }
  set fontColor(value) {
    this.recordChange({prop: "fontColor", value});
  }

  get placeholder() { return this.getProperty("placeholder") }
  set placeholder(value) {
    this.recordChange({prop: "placeholder", value});
    this._needsFit = true;
  }

  get _selection() { return this.getProperty("_selection") }
  set _selection(value) { this.recordChange({prop: "_selection", value}); }

  get selection() { return new TextSelection(this) }

  insertText(pos, str) {
    var oldText = this.textString,
        newText = oldText ? oldText.substr(0, pos) + str + oldText.substr(pos) : str;
    this._needsFit = true;

    this.recordChange({
      prop: "textString", value: newText,
      type: "insert",
      pos: pos,
      str: str
    });
  }

  deleteText(start, end) {
    var oldText = this.textString,
        newText = oldText.substr(0, start) + oldText.substr(end);
    this._needsFit = true;

    this.recordChange({
      prop: "textString", value: newText,
      type: "delete",
      start: start,
      end: end
    });
  }

  selectionOrLineString() {
    var sel = this.selection;
    if (sel.text) return sel.text;
    var line = string.lineIndexComputer(this.textString)(sel.start),
        [start, end] = string.lineNumberToIndexesComputer(this.textString)(line);
    return this.textString.slice(start, end).trim();
  }

  aboutToRender() {
    super.aboutToRender();
    this.fitIfNeeded();
  }

  render(renderer) {
    return renderer.renderText(this);
  }

  fit() {
    var {fixedHeight, fixedWidth} = this;
    if (fixedHeight && fixedWidth) return;

    var {fontMetric, fontFamily, fontSize, placeholder, textString} = this,
        {height: placeholderHeight, width: placeholderWidth} = fontMetric.sizeForStr(fontFamily, fontSize, placeholder || " "),
        {height, width} = fontMetric.sizeForStr(fontFamily, fontSize, textString);
    if (!fixedHeight)
      this.height = Math.max(placeholderHeight, height);
    if (!fixedWidth)
      this.width = Math.max(placeholderWidth, width);
  }

  fitIfNeeded() {
    if (this._needsFit) { this.fit(); this._needsFit = false; }
  }

  recordSelectionFrom(domNode) {
    var { selectionStart: start, selectionEnd: end } = domNode;
    this._selection = { start: start, end: end };
  }

  onMouseDown(evt) { this.onMouseMove(evt); }

  onMouseMove(evt) {
    var { clickedOnMorph, clickedOnPosition } = evt.state;
    if (clickedOnMorph === this) {
      var startPos = this.localize(clickedOnPosition),
          endPos = this.localize(evt.position),
          { fontFamily, fontSize, textString, fontMetric, selection } = this,
          { start: curStart, end: curEnd } = selection,
          start = fontMetric.indexFromPoint(fontFamily, fontSize, textString, startPos),
          end = fontMetric.indexFromPoint(fontFamily, fontSize, textString, endPos);
      if (start > end)
        [start, end] = [end, start];
      if (end !== curEnd || start !== curStart)
        selection.range = { start: start, end: end };
    }
  }

  onKeyUp(evt) {
    switch (evt.keyString()) {
      case 'Command-D': case 'Command-P': evt.stop(); break;
    }
  }

  async onKeyDown(evt) {
    var keyString = evt.keyString(),
        key = evt.domEvt.key,
        sel = this.selection;
    switch (keyString) {
      case 'Command-C': case 'Command-X': case 'Command-V':
        break; // handled by onCut()/onPaste()

      case 'Command-D':
        evt.stop();
        var result = await lively.vm.runEval(this.selectionOrLineString(), {System, targetModule: "lively://test-text/1"});
        this.world()[result.isError ? "logError" : "setStatusMessage"](result.value);
        break;

      case 'Command-P':
        evt.stop();
        var result = await lively.vm.runEval(this.selectionOrLineString(), {System, targetModule: "lively://test-text/1"});
        this.textString = this.textString.slice(0, sel.end) + result.value + this.textString.slice(sel.end);
        break;

      case 'Backspace':
        if (this.readOnly) break;
        evt.stop();
        sel.isCollapsed && sel.start && sel.start--;
        sel.text = "";
        sel.collapse();
        break;

      case 'Left': case 'Right':
        evt.stop();
        sel.start += (keyString === 'Right' ? 1 : (sel.start > 0 ? -1 : 0));
        sel.collapse();
        break;

      case 'Up': case 'Down':
        evt.stop();
        var text = this.textString,
            line = string.lineIndexComputer(text)(sel.start),
            otherLine = line + (keyString === "Down" ? 1 : -1),
            rangeComp = string.lineNumberToIndexesComputer(text),
            [lineStart, lineEnd] = rangeComp(line),
            otherLineRange = rangeComp(otherLine),
            [otherLineStart, otherLineEnd] = otherLineRange || [lineStart, lineEnd];
        sel.start = Math.min(otherLineStart + (sel.start - lineStart), otherLineEnd-1);
        sel.collapse();
        break;

      default:
        if (this.readOnly) break;
        // FIXME!
        evt.stop();
        if (key.length !== 1 && key !== "Space" && key !== "Enter") {
          break;
        }
        sel.text = (key === "Enter" ? "\n" : key);
        sel.collapse(sel.start + 1);
    }
  }

  onCut(evt) {
    this.onCopy(evt);
    var sel = this.selection;
    sel.text = "";
    sel.collapse();
  }

  onCopy(evt) {
    evt.stop();
    evt.domEvt.clipboardData.setData("text", this.selection.text);
  }

  onPaste(evt) {
    var sel = this.selection;
    sel.text = evt.domEvt.clipboardData.getData("text");
    sel.collapse(sel.end);
  }
}

class TextSelection {

  constructor(textMorph) {
    this.textMorph = textMorph;
  }

  get range() { return this.textMorph._selection; }
  set range(rangeObj) {
    let morph = this.textMorph;
    morph._selection = rangeObj;
    morph._needsSelect = true;
  }

  get start() { return this.range.start; }
  set start(val) { this.range = { start: val, end: this.end }; }

  get end() { return this.range.end }
  set end(val) { this.range = { start: this.start, end: val }; }

  get text() { return this.textMorph.textString.substring(this.start, this.end) }
  set text(val) {
    let { start, end } = this.range,
        morph = this.textMorph;
    if (!this.isCollapsed) {
      morph.deleteText(start, end);
    }
    if (val.length) {
      morph.insertText(start, val);
    }
    this.range = { start: this.start, end: this.start + val.length };
  }

  get isCollapsed() { return this.start === this.end; }
  collapse(index = this.start) { this.range = { start: index, end: index }; }
}
<|endoftext|>


<commit_before>
var user1 = {
	id: 1,
	name: 'nick',
	idName: function(){
		var id = 'inner';
		console.log(id+this.name);
	}
};

user1.idName();<commit_msg>Revert "Revert "test revert""<commit_after>
var user1 = {
	id: 1,
	name: 'nick',
	idName: function(){
		var id = 'inner';
		console.log(id+this.name);
	}
};

user1.idName();

//skjdlbflkjsbgj<|endoftext|>


<commit_before>'use strict';

var value          = require('es5-ext/object/valid-value')
  , d              = require('d')
  , unserializeId  = require('../unserialize/id')
  , unserializeKey = require('../unserialize/key')
  , object         = require('../../valid-dbjs-object')

  , defineProperty = Object.defineProperty
  , Result;

var tokenize = function (id) {
	var names = [];
	unserializeId(value(id)).forEach(function (token, index) {
		if (index % 2) {
			if (token !== '/') throw new TypeError(id + " is not property id");
			return;
		}
		names.push(token);
	});
	return names;
};

var resolveObject = function (obj, names, _observe) {
	var sKey, propValue, i = 1;
	for (i = 1; i < names.length; ++i) {
		sKey = names[i - 1];
		propValue = obj._get_(sKey);
		if (propValue == null) {
			if (_observe) _observe(obj._getObservable_(sKey));
			return null;
		}
		obj = object(propValue);
	}
	return obj;
};

Result = function (object, sKey) {
	this.object = object;
	this.sKey = sKey;
};
Object.defineProperties(Result.prototype, {
	descriptor: d.gs(function () { return this.object._getDescriptor_(this.sKey); }),
	ownDescriptor: d.gs(function () { return this.object._getOwnDescriptor_(this.sKey); }),
	observable: d.gs(function () {
		if (this.object.isKeyStatic(this.key)) return this.object[this.key];
		return this.object._getObservable_(this.sKey);
	}),
	value: d.gs(function () { return this.object._get_(this.sKey); }),
	id: d.gs(function () {
		return this.object.__id__ + '/' + this.sKey;
	}),
	key: d.gs(function () {
		defineProperty(this, 'key', d(unserializeKey(this.sKey)));
		return this.key;
	})
});

module.exports = exports = function (obj, id, _observe) {
	var names;
	object(obj);
	names = tokenize(id);
	obj = resolveObject(obj, names, _observe);
	if (!obj) return null;
	return new Result(obj, names[names.length - 1]);
};
exports.tokenize = tokenize;
exports.resolveObject = resolveObject;
exports.Result = Result;
<commit_msg>Expose getObservable<commit_after>'use strict';

var value          = require('es5-ext/object/valid-value')
  , d              = require('d')
  , unserializeId  = require('../unserialize/id')
  , unserializeKey = require('../unserialize/key')
  , object         = require('../../valid-dbjs-object')

  , defineProperty = Object.defineProperty
  , Result;

var tokenize = function (id) {
	var names = [];
	unserializeId(value(id)).forEach(function (token, index) {
		if (index % 2) {
			if (token !== '/') throw new TypeError(id + " is not property id");
			return;
		}
		names.push(token);
	});
	return names;
};

var resolveObject = function (obj, names, _observe) {
	var sKey, propValue, i = 1;
	for (i = 1; i < names.length; ++i) {
		sKey = names[i - 1];
		propValue = obj._get_(sKey);
		if (propValue == null) {
			if (_observe) _observe(obj._getObservable_(sKey));
			return null;
		}
		obj = object(propValue);
	}
	return obj;
};

Result = function (object, sKey) {
	this.object = object;
	this.sKey = sKey;
};
Object.defineProperties(Result.prototype, {
	descriptor: d.gs(function () { return this.object._getDescriptor_(this.sKey); }),
	ownDescriptor: d.gs(function () { return this.object._getOwnDescriptor_(this.sKey); }),
	observable: d.gs(function () {
		if (this.object.isKeyStatic(this.key)) return this.object[this.key];
		return this.object._getObservable_(this.sKey);
	}),
	value: d.gs(function () { return this.object._get_(this.sKey); }),
	id: d.gs(function () {
		return this.object.__id__ + '/' + this.sKey;
	}),
	key: d.gs(function () {
		defineProperty(this, 'key', d(unserializeKey(this.sKey)));
		return this.key;
	})
});

module.exports = exports = function (obj, id, _observe) {
	var names;
	object(obj);
	names = tokenize(id);
	obj = resolveObject(obj, names, _observe);
	if (!obj) return null;
	return new Result(obj, names[names.length - 1]);
};
exports.tokenize = tokenize;
exports.resolveObject = resolveObject;
exports.getObservable = function (obj, id, _observe) {
	var names, key;
	object(obj);
	names = tokenize(id);
	obj = resolveObject(obj, names, _observe);
	if (!obj) return null;
	key = names[names.length - 1];
	if (obj.isKeyStatic(key)) return obj[key];
	return obj._getObservable_(key);
};
exports.Result = Result;
<|endoftext|>


<commit_before>todolist = (function () {
"use strict";

var get = function(id) {
    return document.getElementById(id);
};

function el(tag) {
    var ret = document.createElement(tag);
    var i;

    for (i = 1; i < arguments.length; i++) {
	ret.appendChild(arguments[i]);
    }

    return ret;
}

function t(text) {
    return document.createTextNode(text);
}

function editableList() {
    var ret = el("div");
    var selected;

    function listItem() {
	var item = el("div");
	var entry = el("input");
	var text = t("new item");
	var focused = false;

	item.appendChild(text);

	entry.type = "text";
	entry.className = "listItem";

	item.value = function () {
	    return entry.value;
	};

	item.onclick = function () {
	    if (item === selected) {
		item.focus();
	    } else {
		ret.select(item);
	    }
	};

	item.focus = function () {
	    if (focused) {
		return;
	    }
	    focused = true;
	    entry.value = text.data;
	    item.appendChild(entry);
	    item.removeChild(text);
	    entry.focus();
	};

	item.blur = function () {
	    if (focused) {
		entry.onblur();
	    }
	};

	entry.onblur = function () {
	    if (!focused) {
		return;
	    }
	    focused = false;
	    item.removeChild(entry);
	    item.appendChild((text = t(entry.value)));
	};

	return item;
    };

    ret.select = function (item) {
	if (selected) {
	    selected.className = "listItem";
	    selected.blur();
	}
	selected = item;
	selected.className = "listItem selected";
    };

    ret.add = function () {
	var item = listItem();
	ret.appendChild(item);
	ret.select(item);
    };

    ret.getItems = function () {
	var items = [];
	for (i = ret.firstChild; i; i = i.nextSibling) {
	    items.push(i.value());
	}

	return items;
    };

    ret.remove = function () {
	if (selected) {
	    ret.removeChild(selected);
	}
    };

    return ret;
}

function isTouchDevice(){
    try {
	document.createEvent("TouchEvent");
	return true;
    } catch(e) {
	return false;
    }
}

function mobileScrollFix(element) {
    var scrollStartPos = 0;

    function touchstart(event) {
	scrollStartPos = this.scrollTop + event.touches[0].pageY;
	//event.preventDefault();
    }

    function touchmove(event) {
	this.scrollTop = scrollStartPos - event.touches[0].pageY;
	event.preventDefault();
    }

    if (isTouchDevice()) {
	element.ontouchstart = touchstart;
	element.ontouchmove = touchmove;
    }
}

var doneBtn = get("done");
var dropBtn = get("drop");
var newBtn = get("new");
var list = editableList();
get("list").appendChild(list);
mobileScrollFix(get("screen"));

newBtn.onclick = function () {
    list.add();
};

dropBtn.onclick = function () {
    list.remove();
};

})();<commit_msg>use html contentEditable instead of dynamic replacment of elements<commit_after>todolist = (function () {
"use strict";

var get = function(id) {
    return document.getElementById(id);
};

function el(tag) {
    var ret = document.createElement(tag);
    var i;

    for (i = 1; i < arguments.length; i++) {
	ret.appendChild(arguments[i]);
    }

    return ret;
}

function t(text) {
    return document.createTextNode(text);
}

function editableList() {
    var ret = el("div");
    var selected;

    function listItem() {
	var item = el("div");
	var text = t("");

	item.appendChild(text);
	item.contentEditable = "true";
	item.className = "listItem";

	item.onclick = function () {
	    ret.select(item);
	};

	item.onkeypress = function (evt) {
	    switch (evt.keyCode) {
	    case 13:
		ret.add();
		evt.preventDefault();
		break;
	    case 40:
		if (selected && selected.nextSibling) {
		    ret.select(selected.nextSibling);
		}
 		break;
	    case 38:
		if (selected && selected.prevSibling) {
		    ret.selected(selected.prevSibling);
		}
		break;

	    };
	};

	return item;
    };

    ret.select = function (item) {
	selected = item;
	document.getSelection().selectAllChildren(item);
    };

    ret.add = function () {
	var item = listItem();
	if (selected) {
	    ret.insertBefore(item, selected.nextSibling);
	} else {
	    ret.appendChild(item);
	}
	ret.select(item);
	item.focus();
    };

    ret.remove = function () {
	var next = selected ?
	    (selected.nextSibling || selected.previousSibling) :
	    null;

	if (selected) {
	    ret.removeChild(selected);
	    if (next) {
		ret.select(next);
	    }
	}
    };

    return ret;
}

function isTouchDevice(){
    try {
	document.createEvent("TouchEvent");
	return true;
    } catch(e) {
	return false;
    }
}

function mobileScrollFix(element) {
    var scrollStartPos = 0;

    function touchstart(event) {
	scrollStartPos = this.scrollTop + event.touches[0].pageY;
	//event.preventDefault();
    }

    function touchmove(event) {
	this.scrollTop = scrollStartPos - event.touches[0].pageY;
	event.preventDefault();
    }

    if (isTouchDevice()) {
	element.ontouchstart = touchstart;
	element.ontouchmove = touchmove;
    }
}

var doneBtn = get("done");
var dropBtn = get("drop");
var newBtn = get("new");
var list = editableList();
get("list").appendChild(list);
mobileScrollFix(get("screen"));

newBtn.onclick = function () {
    list.add();
};

dropBtn.onclick = function () {
    list.remove();
};

})();<|endoftext|>


<commit_before>$(document).ready(function(){
    var showNewTaskForm = function() {
        var divs = $('#add').children('div');
        if (!divs.first().is(':hidden')) {
            $(divs.first()).hide();
            $(divs.get(1)).show().find('#new-task').focus();
        }
    };

    var hideNewTaskForm = function() {
        var divs = $('#add div');
        if (divs.first().is(':hidden')) {
            $(divs.get(1)).hide();
            $(divs.first()).show();
        }
    };

    $('#add').click(showNewTaskForm);

    $('#add form').submit(function(){
        var newTask = $('#new-task').val();
        $('#new-task').val('');
        var tasks = window.appStorage.tasks;
        tasks.push({text:newTask});
        window.appStorage.setItem('tasks', tasks);
        $('#task-list ol li#add').before("
" + newTask + "
")
        return false;
    });

    $('#new-task').
        keydown(function(event){
            if (event.keyCode == 27) {
                hideNewTaskForm();
            }
        });

    $('.task').live('click', function(){
        $(this).addClass('completed');
        var clicked = this;
        $('#task-list li').each(function(index){
            if (this == clicked) {
                var tasks = window.appStorage.tasks;
                tasks.splice(index, 1);
                window.appStorage.tasks = tasks;
            }
        });
        $(clicked).delay(1000).slideUp('slow');
    });

    $('#twitter-login a').click(function(){
        window.appStorage.$.connect({twitter:true});
    });

    $('#login form').submit(function(){
        $('#login-button').attr('disabled', true);
        window.appStorage.$.connect({email: $('#email').val()});
        return false;
    });

    $('#login').show();

    window.appStorage.$.ready(function(){
        var tasks = window.appStorage.tasks;

        if (!tasks) {
            tasks = [
                {text: 'This is an example todo task'},
                {text: 'Click on a task to mark it completed'}
            ];
            window.appStorage.setItem('tasks', tasks);
        }

        for (var i=0; i<tasks.length; i++) {
            $('#task-list li#add').before("
" + tasks[i].text + "
")
        }

        $('#login').hide();
        $('#task-list').show();
        $(document).keypress(showNewTaskForm);
    });

});
<commit_msg>migrate old task schema<commit_after>$(document).ready(function(){
    var showNewTaskForm = function() {
        var divs = $('#add').children('div');
        if (!divs.first().is(':hidden')) {
            $(divs.first()).hide();
            $(divs.get(1)).show().find('#new-task').focus();
        }
    };

    var hideNewTaskForm = function() {
        var divs = $('#add div');
        if (divs.first().is(':hidden')) {
            $(divs.get(1)).hide();
            $(divs.first()).show();
        }
    };

    $('#add').click(showNewTaskForm);

    $('#add form').submit(function(){
        var newTask = $('#new-task').val();
        $('#new-task').val('');
        var tasks = window.appStorage.tasks;
        tasks.push({text:newTask});
        window.appStorage.setItem('tasks', tasks);
        $('#task-list ol li#add').before("
" + newTask + "
")
        return false;
    });

    $('#new-task').
        keydown(function(event){
            if (event.keyCode == 27) {
                hideNewTaskForm();
            }
        });

    $('.task').live('click', function(){
        $(this).addClass('completed');
        var clicked = this;
        $('#task-list li').each(function(index){
            if (this == clicked) {
                var tasks = window.appStorage.tasks;
                tasks.splice(index, 1);
                window.appStorage.tasks = tasks;
            }
        });
        $(clicked).delay(1000).slideUp('slow');
    });

    $('#twitter-login a').click(function(){
        window.appStorage.$.connect({twitter:true});
    });

    $('#login form').submit(function(){
        $('#login-button').attr('disabled', true);
        window.appStorage.$.connect({email: $('#email').val()});
        return false;
    });

    $('#login').show();

    window.appStorage.$.ready(function(){
        var tasks = window.appStorage.tasks;

        if (!tasks) {
            tasks = [
                {text: 'This is an example todo task'},
                {text: 'Click on a task to mark it completed'}
            ];
            window.appStorage.setItem('tasks', tasks);

        } else {
            // migrate old schema
            var updated = false;
            for (var i=0; i<tasks.length; i++) {
                if (!('text' in task[i])) {
                    task[i] = {text: task[i]};
                    updated = true;
                }
            }
            if (updated) {
                window.appStorage.setItem('tasks', tasks);
            }
        }

        for (var i=0; i<tasks.length; i++) {
            $('#task-list li#add').before("
" + tasks[i].text + "
")
        }

        $('#login').hide();
        $('#task-list').show();
        $(document).keypress(showNewTaskForm);
    });

});
<|endoftext|>


<commit_before>import Ember from 'ember';
import moment from 'moment';

export default Ember.Component.extend({
  classNames: ['form-group'],
  attributeBindings: ['start', 'end', 'serverFormat'],
  start: undefined,
  end: undefined,
  timePicker: false,
  format: 'MMM D, YYYY',
  serverFormat: 'YYYY-MM-DD',
  rangeText: Ember.computed(function () {
    let format = this.get('format');
    let serverFormat = this.get('serverFormat');
    let start = this.get('start');
    let end = this.get('end');
    if (!Ember.isEmpty(start) && !Ember.isEmpty(end)) {
      return moment(start, serverFormat).format(format) + this.get('separator') + moment(end, serverFormat).format(format);
    }
    return '';
  }),
  opens: null,
  drops: null,
  separator: ' - ',
  singleDatePicker: false,
  placeholder: null,
  buttonClasses: ['btn'],
  applyClass: null,
  cancelClass: null,
  ranges: {
    'Today': [moment(), moment()],
    'Yesterday': [moment().subtract(1, 'days'), moment().subtract(1, 'days')],
    'Last 7 Days': [moment().subtract(6, 'days'), moment()],
    'Last 30 Days': [moment().subtract(29, 'days'), moment()],
    'This Month': [moment().startOf('month'), moment().endOf('month')],
    'Last Month': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')]
  },
  removeDropdownOnDestroy: false,
  cancelLabel: 'Cancel',
  applyAction: null,
  cancelAction: null,

  //Init the dropdown when the component is added to the DOM
  didInsertElement: function() {
    var self = this;

    let momentStartDate = moment(this.get('start'), this.get('serverFormat'));
    let momentEndDate = moment(this.get('end'), this.get('serverFormat'));
    let startDate = momentStartDate.isValid() ? momentStartDate : undefined;
    let endDate = momentEndDate.isValid() ? momentEndDate : undefined;

    this.$('.daterangepicker-input').daterangepicker({
      locale: {
        cancelLabel: this.get('cancelLabel')
      },
      format: this.get('format'),
      startDate: startDate,
      endDate: endDate,
      timePicker: this.get('timePicker'),
      ranges: this.get('ranges'),
      buttonClasses: this.get('buttonClasses'),
      applyClass: this.get('applyClass'),
      cancelClass: this.get('cancelClass'),
      separator: this.get('separator'),
      singleDatePicker: this.get('singleDatePicker'),
      drops: this.get('drops'),
      opens: this.get('opens')
    });

    this.$('.daterangepicker-input').on('show.daterangepicker', function(ev, picker) {
      let serverFormat = self.get(serverFormat);
      let startDate = moment(self.get('start'), serverFormat);
      let endDate = moment(self.get('end'), serverFormat);

      picker.setStartDate(startDate);
      picker.setEndDate(endDate);
    });

    this.$('.daterangepicker-input').on('apply.daterangepicker', function(ev, picker) {
      var start = picker.startDate.format(self.get('serverFormat'));
      var end = picker.endDate.format(self.get('serverFormat'));
      var applyAction = self.get('applyAction');

      if (applyAction) {
        Ember.assert(
          'applyAction for date-range-picker must be a function',
          typeof applyAction === 'function'
        );
        applyAction(start, end);
      } else {
        self.setProperties({start, end});
      }
    });

    this.$('.daterangepicker-input').on('cancel.daterangepicker', function() {
      var cancelAction = self.get('cancelAction');

      if (cancelAction) {
        Ember.assert(
          'cancelAction for date-range-picker must be a function',
          typeof cancelAction === 'function'
        );
        cancelAction();
      } else {
        self.set('start', self.get('start'));
        self.set('end', self.get('end'));
      }
    });

  },

  //Remove the hidden dropdown when this component is destroyed
  willDestroy: function () {
    if (this.get('removeDropdownOnDestroy')) {
      Ember.$('.daterangepicker').remove();
    }
  }
});
<commit_msg>Fix serverFormat being undefined<commit_after>import Ember from 'ember';
import moment from 'moment';

export default Ember.Component.extend({
  classNames: ['form-group'],
  attributeBindings: ['start', 'end', 'serverFormat'],
  start: undefined,
  end: undefined,
  timePicker: false,
  format: 'MMM D, YYYY',
  serverFormat: 'YYYY-MM-DD',
  rangeText: Ember.computed(function () {
    let format = this.get('format');
    let serverFormat = this.get('serverFormat');
    let start = this.get('start');
    let end = this.get('end');
    if (!Ember.isEmpty(start) && !Ember.isEmpty(end)) {
      return moment(start, serverFormat).format(format) + this.get('separator') + moment(end, serverFormat).format(format);
    }
    return '';
  }),
  opens: null,
  drops: null,
  separator: ' - ',
  singleDatePicker: false,
  placeholder: null,
  buttonClasses: ['btn'],
  applyClass: null,
  cancelClass: null,
  ranges: {
    'Today': [moment(), moment()],
    'Yesterday': [moment().subtract(1, 'days'), moment().subtract(1, 'days')],
    'Last 7 Days': [moment().subtract(6, 'days'), moment()],
    'Last 30 Days': [moment().subtract(29, 'days'), moment()],
    'This Month': [moment().startOf('month'), moment().endOf('month')],
    'Last Month': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')]
  },
  removeDropdownOnDestroy: false,
  cancelLabel: 'Cancel',
  applyAction: null,
  cancelAction: null,

  //Init the dropdown when the component is added to the DOM
  didInsertElement: function() {
    var self = this;

    let momentStartDate = moment(this.get('start'), this.get('serverFormat'));
    let momentEndDate = moment(this.get('end'), this.get('serverFormat'));
    let startDate = momentStartDate.isValid() ? momentStartDate : undefined;
    let endDate = momentEndDate.isValid() ? momentEndDate : undefined;

    this.$('.daterangepicker-input').daterangepicker({
      locale: {
        cancelLabel: this.get('cancelLabel')
      },
      format: this.get('format'),
      startDate: startDate,
      endDate: endDate,
      timePicker: this.get('timePicker'),
      ranges: this.get('ranges'),
      buttonClasses: this.get('buttonClasses'),
      applyClass: this.get('applyClass'),
      cancelClass: this.get('cancelClass'),
      separator: this.get('separator'),
      singleDatePicker: this.get('singleDatePicker'),
      drops: this.get('drops'),
      opens: this.get('opens')
    });

    this.$('.daterangepicker-input').on('show.daterangepicker', function(ev, picker) {
      let serverFormat = self.get('serverFormat');
      let startDate = moment(self.get('start'), serverFormat);
      let endDate = moment(self.get('end'), serverFormat);

      picker.setStartDate(startDate);
      picker.setEndDate(endDate);
    });

    this.$('.daterangepicker-input').on('apply.daterangepicker', function(ev, picker) {
      var start = picker.startDate.format(self.get('serverFormat'));
      var end = picker.endDate.format(self.get('serverFormat'));
      var applyAction = self.get('applyAction');

      if (applyAction) {
        Ember.assert(
          'applyAction for date-range-picker must be a function',
          typeof applyAction === 'function'
        );
        applyAction(start, end);
      } else {
        self.setProperties({start, end});
      }
    });

    this.$('.daterangepicker-input').on('cancel.daterangepicker', function() {
      var cancelAction = self.get('cancelAction');

      if (cancelAction) {
        Ember.assert(
          'cancelAction for date-range-picker must be a function',
          typeof cancelAction === 'function'
        );
        cancelAction();
      } else {
        self.set('start', self.get('start'));
        self.set('end', self.get('end'));
      }
    });

  },

  //Remove the hidden dropdown when this component is destroyed
  willDestroy: function () {
    if (this.get('removeDropdownOnDestroy')) {
      Ember.$('.daterangepicker').remove();
    }
  }
});
<|endoftext|>


<commit_before>export function initialize(container, application) {
  application.register('google:main', window.google, { instantiate: false });
  application.inject('component', 'google', 'google:main');
}

export default {
  name: 'register-google',
  initialize: initialize
};
<commit_msg>Fix initializer arity deprecation in ember >2.1<commit_after>export function initialize() {
  let application = arguments[1] || arguments[0];

  application.register('google:main', window.google, { instantiate: false });
  application.inject('component', 'google', 'google:main');
}

export default {
  name: 'register-google',
  initialize: initialize
};
<|endoftext|>


<commit_before>require(['//cdn.jsdelivr.net/intro.js/0.9.0/intro.min.js'], function(introJs) {
    var tours = window.tours;

    function getCurrentSteps() {
        var path = window.location.pathname;
        for (var tour in tours) {
            if (new RegExp(tour).test(path)) {
                return tours[tour].steps;
            }
        }
    }

    function startIntro() {
        var steps = getCurrentSteps();
        if (!steps) {
            // Couldn't find tour, bail.
            return;
        }
        var intro = introJs();
        intro.setOptions({
            steps: getCurrentSteps()
        });
        intro.start();
    }

    var tourBtn = document.createElement('a');
    tourBtn.className = 'btn tour-btn';
    tourBtn.onclick = startIntro;
    document.body.appendChild(tourBtn);
});
<commit_msg>only create button if we have a tour<commit_after>require(['//cdn.jsdelivr.net/intro.js/0.9.0/intro.min.js'], function(introJs) {
    var tours = window.tours;

    function getCurrentSteps() {
        var path = window.location.pathname;
        for (var tour in tours) {
            if (new RegExp(tour).test(path)) {
                return tours[tour].steps;
            }
        }
    }

    function startIntro(steps) {
        var intro = introJs();
        intro.setOptions({
            steps: steps
        });
        intro.start();
    }

    function createTourButton(steps) {
        var tourBtn = document.createElement('a');
        tourBtn.className = 'btn tour-btn';
        tourBtn.onclick = function() {
            startIntro(steps);
        };
        document.body.appendChild(tourBtn);
    }

    var currentSteps = getCurrentSteps();
    if (currentSteps) {
        createTourButton(currentSteps);
    }

});
<|endoftext|>


<commit_before>/*---------------------------------------------------------
 * OpenERP web library
 *---------------------------------------------------------*/

openerp.web.view_tree = function(openerp) {
var QWeb = openerp.web.qweb,
      _lt = openerp.web._lt;

openerp.web.views.add('tree', 'openerp.web.TreeView');
openerp.web.TreeView = openerp.web.View.extend(/** @lends openerp.web.TreeView# */{
    display_name: _lt('Tree'),
    /**
     * Indicates that this view is not searchable, and thus that no search
     * view should be displayed (if there is one active).
     */
    searchable : false,
    /**
     * Genuine tree view (the one displayed as a tree, not the list)
     *
     * @constructs openerp.web.TreeView
     * @extends openerp.web.View
     *
     * @param parent
     * @param dataset
     * @param view_id
     * @param options
     */
    init: function(parent, dataset, view_id, options) {
        this._super(parent);
        this.dataset = dataset;
        this.model = dataset.model;
        this.view_id = view_id;

        this.records = {};

        this.options = _.extend({}, this.defaults, options || {});

        _.bindAll(this, 'color_for');
    },

    start: function () {
        this._super();
        return this.rpc("/web/treeview/load", {
            model: this.model,
            view_id: this.view_id,
            view_type: "tree",
            toolbar: this.view_manager ? !!this.view_manager.sidebar : false,
            context: this.dataset.get_context()
        }, this.on_loaded);
    },
    /**
     * Returns the list of fields needed to correctly read objects.
     *
     * Gathers the names of all fields in fields_view_get, and adds the
     * field_parent (children_field in the tree view) if it's not already one
     * of the fields to fetch
     *
     * @returns {Array} an array of fields which can be provided to DataSet.read_slice and others
     */
    fields_list: function () {
        var fields = _.keys(this.fields);
        if (!_(fields).contains(this.children_field)) {
            fields.push(this.children_field);
        }
        return fields;
    },
    on_loaded: function (fields_view) {
        var self = this;
        var has_toolbar = !!fields_view.arch.attrs.toolbar;
        // field name in OpenERP is kinda stupid: this is the name of the field
        // holding the ids to the children of the current node, why call it
        // field_parent?
        this.children_field = fields_view['field_parent'];
        this.fields_view = fields_view;
        _(this.fields_view.arch.children).each(function (field) {
            if (field.attrs.modifiers) {
                field.attrs.modifiers = JSON.parse(field.attrs.modifiers);
            }
        });
        this.fields = fields_view.fields;
        this.hook_row_click();
        this.$element.html(QWeb.render('TreeView', {
            'title': this.fields_view.arch.attrs.string,
            'fields_view': this.fields_view.arch.children,
            'fields': this.fields,
            'toolbar': has_toolbar
        }));

        this.dataset.read_slice(this.fields_list()).then(function(records) {
            if (!has_toolbar) {
                // WARNING: will do a second read on the same ids, but only on
                //          first load so not very important
                self.getdata(null, _(records).pluck('id'));
                return;
            }

            var $select = self.$element.find('select')
                .change(function () {
                    var $option = $(this).find(':selected');
                    self.getdata($option.val(), $option.data('children'));
                });
            _(records).each(function (record) {
                self.records[record.id] = record;
                $('<option>')
                        .val(record.id)
                        .text(record.name)
                        .data('children', record[self.children_field])
                    .appendTo($select);
            });

            if (!_.isEmpty(records)) {
                $select.change();
            }
        });

        // TODO store open nodes in url ?...
        this.do_push_state({});

        if (!this.fields_view.arch.attrs.colors) {
            return;
        }
        this.colors = _(this.fields_view.arch.attrs.colors.split(';')).chain()
            .compact()
            .map(function(color_pair) {
                var pair = color_pair.split(':'),
                    color = pair[0],
                    expr = pair[1];
                return [color, py.parse(py.tokenize(expr)), expr];
            }).value();
    },
    /**
     * Returns the color for the provided record in the current view (from the
     * ``@colors`` attribute)
     *
     * @param {Object} record record for the current row
     * @returns {String} CSS color declaration
     */
    color_for: function (record) {
        if (!this.colors) { return ''; }
        var context = _.extend({}, record, {
            uid: this.session.uid,
            current_date: new Date().toString('yyyy-MM-dd')
            // TODO: time, datetime, relativedelta
        });
        for(var i=0, len=this.colors.length; i<len; ++i) {
            var pair = this.colors[i],
                color = pair[0],
                expression = pair[1];
            if (py.evaluate(expression, _.extend({bool: py.bool}, context))) {
                return 'color: ' + color + ';';
            }
            // TODO: handle evaluation errors
        }
        return '';
    },
    /**
     * Sets up opening a row
     */
    hook_row_click: function () {
        var self = this;
        this.$element.delegate('.treeview-td span, .treeview-tr span', 'click', function (e) {
            e.stopImmediatePropagation();
            self.activate($(this).closest('tr').data('id'));
        });

        this.$element.delegate('.treeview-tr', 'click', function () {
            var is_loaded = 0,
                $this = $(this),
                record_id = $this.data('id'),
                record = self.records[record_id],
                children_ids = record[self.children_field];

            _(children_ids).each(function(childid) {
                if (self.$element.find('#treerow_' + childid).length) {
                    if (self.$element.find('#treerow_' + childid).is(':hidden')) {
                        is_loaded = -1;
                    } else {
                        is_loaded++;
                    }
                }
            });
            if (is_loaded === 0) {
                if (!$this.parent().hasClass('oe-open')) {
                    self.getdata(record_id, children_ids);
                }
            } else {
                self.showcontent(record_id, is_loaded < 0);
            }
        });
    },
    // get child data of selected value
    getdata: function (id, children_ids) {
        var self = this;

        self.dataset.read_ids(children_ids, this.fields_list()).then(function(records) {
            _(records).each(function (record) {
                self.records[record.id] = record;
            });

            var $curr_node = self.$element.find('#treerow_' + id);
            var children_rows = QWeb.render('TreeView.rows', {
                'records': records,
                'children_field': self.children_field,
                'fields_view': self.fields_view.arch.children,
                'fields': self.fields,
                'level': $curr_node.data('level') || 0,
                'render': openerp.web.format_value,
                'color_for': self.color_for
            });

            if ($curr_node.length) {
                $curr_node.addClass('oe-open');
                $curr_node.after(children_rows);
            } else {
                self.$element.find('tbody').html(children_rows);
            }
        });
    },

    // Get details in listview
    activate: function(id) {
        var self = this;
        var local_context = {
            active_model: self.dataset.model,
            active_id: id,
            active_ids: [id]};
        this.rpc('/web/treeview/action', {
            id: id,
            model: this.dataset.model,
            context: new openerp.web.CompoundContext(
                this.dataset.get_context(), local_context)
        }, function (actions) {
            if (!actions.length) { return; }
            var action = actions[0][2];
            var c = new openerp.web.CompoundContext(local_context);
            if (action.context) {
                c.add(action.context);
            }
            action.context = c;
            self.do_action(action);
        });
    },

    // show & hide the contents
    showcontent: function (record_id, show) {
        this.$element.find('#treerow_' + record_id)
                .toggleClass('oe-open', show);

        _(this.records[record_id][this.children_field]).each(function (child_id) {
            var $child_row = this.$element.find('#treerow_' + child_id);
            if ($child_row.hasClass('oe-open')) {
                this.showcontent(child_id, false);
            }
            $child_row.toggle(show);
        }, this);
    },

    do_show: function () {
        this.$element.show();
    },

    do_hide: function () {
        this.$element.hide();
        this.hidden = true;
    }
});
};
<commit_msg>[FIX] evaluate compound context from treeview before executing action<commit_after>/*---------------------------------------------------------
 * OpenERP web library
 *---------------------------------------------------------*/

openerp.web.view_tree = function(openerp) {
var QWeb = openerp.web.qweb,
      _lt = openerp.web._lt;

openerp.web.views.add('tree', 'openerp.web.TreeView');
openerp.web.TreeView = openerp.web.View.extend(/** @lends openerp.web.TreeView# */{
    display_name: _lt('Tree'),
    /**
     * Indicates that this view is not searchable, and thus that no search
     * view should be displayed (if there is one active).
     */
    searchable : false,
    /**
     * Genuine tree view (the one displayed as a tree, not the list)
     *
     * @constructs openerp.web.TreeView
     * @extends openerp.web.View
     *
     * @param parent
     * @param dataset
     * @param view_id
     * @param options
     */
    init: function(parent, dataset, view_id, options) {
        this._super(parent);
        this.dataset = dataset;
        this.model = dataset.model;
        this.view_id = view_id;

        this.records = {};

        this.options = _.extend({}, this.defaults, options || {});

        _.bindAll(this, 'color_for');
    },

    start: function () {
        this._super();
        return this.rpc("/web/treeview/load", {
            model: this.model,
            view_id: this.view_id,
            view_type: "tree",
            toolbar: this.view_manager ? !!this.view_manager.sidebar : false,
            context: this.dataset.get_context()
        }, this.on_loaded);
    },
    /**
     * Returns the list of fields needed to correctly read objects.
     *
     * Gathers the names of all fields in fields_view_get, and adds the
     * field_parent (children_field in the tree view) if it's not already one
     * of the fields to fetch
     *
     * @returns {Array} an array of fields which can be provided to DataSet.read_slice and others
     */
    fields_list: function () {
        var fields = _.keys(this.fields);
        if (!_(fields).contains(this.children_field)) {
            fields.push(this.children_field);
        }
        return fields;
    },
    on_loaded: function (fields_view) {
        var self = this;
        var has_toolbar = !!fields_view.arch.attrs.toolbar;
        // field name in OpenERP is kinda stupid: this is the name of the field
        // holding the ids to the children of the current node, why call it
        // field_parent?
        this.children_field = fields_view['field_parent'];
        this.fields_view = fields_view;
        _(this.fields_view.arch.children).each(function (field) {
            if (field.attrs.modifiers) {
                field.attrs.modifiers = JSON.parse(field.attrs.modifiers);
            }
        });
        this.fields = fields_view.fields;
        this.hook_row_click();
        this.$element.html(QWeb.render('TreeView', {
            'title': this.fields_view.arch.attrs.string,
            'fields_view': this.fields_view.arch.children,
            'fields': this.fields,
            'toolbar': has_toolbar
        }));

        this.dataset.read_slice(this.fields_list()).then(function(records) {
            if (!has_toolbar) {
                // WARNING: will do a second read on the same ids, but only on
                //          first load so not very important
                self.getdata(null, _(records).pluck('id'));
                return;
            }

            var $select = self.$element.find('select')
                .change(function () {
                    var $option = $(this).find(':selected');
                    self.getdata($option.val(), $option.data('children'));
                });
            _(records).each(function (record) {
                self.records[record.id] = record;
                $('<option>')
                        .val(record.id)
                        .text(record.name)
                        .data('children', record[self.children_field])
                    .appendTo($select);
            });

            if (!_.isEmpty(records)) {
                $select.change();
            }
        });

        // TODO store open nodes in url ?...
        this.do_push_state({});

        if (!this.fields_view.arch.attrs.colors) {
            return;
        }
        this.colors = _(this.fields_view.arch.attrs.colors.split(';')).chain()
            .compact()
            .map(function(color_pair) {
                var pair = color_pair.split(':'),
                    color = pair[0],
                    expr = pair[1];
                return [color, py.parse(py.tokenize(expr)), expr];
            }).value();
    },
    /**
     * Returns the color for the provided record in the current view (from the
     * ``@colors`` attribute)
     *
     * @param {Object} record record for the current row
     * @returns {String} CSS color declaration
     */
    color_for: function (record) {
        if (!this.colors) { return ''; }
        var context = _.extend({}, record, {
            uid: this.session.uid,
            current_date: new Date().toString('yyyy-MM-dd')
            // TODO: time, datetime, relativedelta
        });
        for(var i=0, len=this.colors.length; i<len; ++i) {
            var pair = this.colors[i],
                color = pair[0],
                expression = pair[1];
            if (py.evaluate(expression, _.extend({bool: py.bool}, context))) {
                return 'color: ' + color + ';';
            }
            // TODO: handle evaluation errors
        }
        return '';
    },
    /**
     * Sets up opening a row
     */
    hook_row_click: function () {
        var self = this;
        this.$element.delegate('.treeview-td span, .treeview-tr span', 'click', function (e) {
            e.stopImmediatePropagation();
            self.activate($(this).closest('tr').data('id'));
        });

        this.$element.delegate('.treeview-tr', 'click', function () {
            var is_loaded = 0,
                $this = $(this),
                record_id = $this.data('id'),
                record = self.records[record_id],
                children_ids = record[self.children_field];

            _(children_ids).each(function(childid) {
                if (self.$element.find('#treerow_' + childid).length) {
                    if (self.$element.find('#treerow_' + childid).is(':hidden')) {
                        is_loaded = -1;
                    } else {
                        is_loaded++;
                    }
                }
            });
            if (is_loaded === 0) {
                if (!$this.parent().hasClass('oe-open')) {
                    self.getdata(record_id, children_ids);
                }
            } else {
                self.showcontent(record_id, is_loaded < 0);
            }
        });
    },
    // get child data of selected value
    getdata: function (id, children_ids) {
        var self = this;

        self.dataset.read_ids(children_ids, this.fields_list()).then(function(records) {
            _(records).each(function (record) {
                self.records[record.id] = record;
            });

            var $curr_node = self.$element.find('#treerow_' + id);
            var children_rows = QWeb.render('TreeView.rows', {
                'records': records,
                'children_field': self.children_field,
                'fields_view': self.fields_view.arch.children,
                'fields': self.fields,
                'level': $curr_node.data('level') || 0,
                'render': openerp.web.format_value,
                'color_for': self.color_for
            });

            if ($curr_node.length) {
                $curr_node.addClass('oe-open');
                $curr_node.after(children_rows);
            } else {
                self.$element.find('tbody').html(children_rows);
            }
        });
    },

    // Get details in listview
    activate: function(id) {
        var self = this;
        var local_context = {
            active_model: self.dataset.model,
            active_id: id,
            active_ids: [id]};
        return this.rpc('/web/treeview/action', {
            id: id,
            model: this.dataset.model,
            context: new openerp.web.CompoundContext(
                this.dataset.get_context(), local_context)
        }).pipe(function (actions) {
            if (!actions.length) { return; }
            var action = actions[0][2];
            var c = new openerp.web.CompoundContext(local_context);
            if (action.context) {
                c.add(action.context);
            }
            return self.rpc('/web/session/eval_domain_and_context', {
                contexts: [c], domains: []
            }).pipe(function (res) {
                action.context = res.context;
                return self.do_action(action);
            }, null);
        }, null);
    },

    // show & hide the contents
    showcontent: function (record_id, show) {
        this.$element.find('#treerow_' + record_id)
                .toggleClass('oe-open', show);

        _(this.records[record_id][this.children_field]).each(function (child_id) {
            var $child_row = this.$element.find('#treerow_' + child_id);
            if ($child_row.hasClass('oe-open')) {
                this.showcontent(child_id, false);
            }
            $child_row.toggle(show);
        }, this);
    },

    do_show: function () {
        this.$element.show();
    },

    do_hide: function () {
        this.$element.hide();
        this.hidden = true;
    }
});
};
<|endoftext|>


<commit_before>/*
 * tree.js
 *
 * This file contains following functionalities
 *
 * Login manager
 *  - handles login process and cookies saving and loading to preserve logged in usr on the local machine
 *  - keeps info about logged in user with
 *
 * WTUser object
 * - properties: id:int, name: str
 * - methods: isLoggedIn() -> bool
 *
 * Session Manager
 *  - handles app status (lastly logged user Name and ID, lastly used view ID) and preserves it using cookies
 *  - takes control over Login manager and calls login method during app start
 *
 * View
 *  - contains basic methods and at the same time serves as a template for class based views
 *
 * View Registry
 *  - serves as main object that orchestrates everything
 *  - usage is demonstrated in index.html
 *
 * ---------------------------------------------------------------------------------------------------------------------
 *
 * How to add create new view and register it:
 *
 *  1. [class based] Create ancestor of View class and override method
 *    - meta: use you own title, description and docs
 *    - init: use your own implementation of view
 *
 *  or
 *
 *  1. [prototype based] Create similar structure as in class based approach and add
 *     following code into the constructor:
 *
 *     Object.assign(this, this?.meta()); // this will spread object into object fields for easier access
 *
 *     alternativelly, you can create those fields directly in constructor, e.g.: this.title = "Template view"
 *
 *  2. register view in ViewRegistry
 *
 *     a) link script file in header section of index.html, e.g.:
 *
 *        <script src="views/new_view/NewView.js"></script>
 *
 *     b) create new entry ("new-view-id": <NewViewObject>) in first parameter of ViewRegistry constructor (also in index.html), e.g.:
 *
 *        "new-view-id": new NewViewObject(),
 *
 *  3. Enjoy your newly registered view ;-)
 *
 */

window.View = class View {
    constructor() {
        this.id = null;
        Object.assign(this, this?.meta()); // this will spread object into object fields for easier access
    }

    meta() {
        return {
            title: "Template view",
            description: "Showcase of the views and their registration.",
            docs: "https://example.com",
        };
    }

    init(container_selector, person_id) {
        document.querySelector(container_selector).innerHTML = `Template View for person with ID: ${person_id}`;
    }
};

/*
 * The ViewRegistry holds the configuration for our collection of different views, builds the <select> field to change between them,
 * and launches the selected view when the "Go" button is clicked.
 */
window.ViewRegistry = class ViewRegistry {
    // These are all divs in index.html holding the various content sections.
    VIEW_SELECT = "#view-select";
    WT_ID_TEXT = "#wt-id-text";
    SHOW_BTN = "#show-btn";
    VIEW_CONTAINER = "#view-container";
    VIEW_TITLE = "#view-title";
    VIEW_DESCRIPTION = " #view-description";
    NAME_PLACEHOLDER = "#name-placeholder";
    WT_ID_LINK = " #wt-id-link";
    VIEW_LOADER = "#view-loader";
    WT_STATUS = "#wt-status";

    // index.html starts with a script that creates a new ViewRegistry, and then immediately calls .render() to update the selection form.
    constructor(views, session_manager) {
        this.views = views;
        this.session = session_manager;

        // This auto-launches the previously selected view (if there was one) when the page reloads.
        const orig_onLoggedIn_cb = this.session.lm.events?.onLoggedIn;
        this.session.lm.events["onLoggedIn"] = (user) => {
            if (!this.session.getHashParams(location.hash).get("name")) {
                document.querySelector(this.WT_ID_TEXT).value = user.name;
            }

            orig_onLoggedIn_cb(user);
            document.querySelector(this.SHOW_BTN).click();
        };

        if (location.hash) {
            this.session.loadUrlHash(Object.keys(views), location.hash);
        }

        addEventListener("hashchange", (e) => this.onHashChange(e));
    }

    onHashChange(e) {
        this.session.loadUrlHash(Object.keys(this.views), e.target.location.hash);

        document.querySelector(this.WT_ID_TEXT).value = this.session.personName;
        document.querySelector(this.VIEW_SELECT).value = this.session.viewID;
        document.querySelector(this.SHOW_BTN).click();
    }

    // Build the <select> option list from the individual views in the registry.
    // Add an event listener to the "go" button to call onSubmit() when clicked.
    // Fill in some data from the logged-in user.
    render() {
        let views = this.views;
        const options = Object.keys(this.views)
            .sort(function (a, b) {
                // We want the base/core option to always be at the top of the drop-down
                if (a == "wt-dynamic-tree") {
                    return -1;
                }
                if (b == "wt-dynamic-tree") {
                    return 1;
                }

                // Sort the rest alphabetically by title
                return views[a].title.localeCompare(views[b].title);
            })
            .map((id) => `<option value="${id}">${this.views[id].title}</option>`)
            .join("");

        const submitBtn = document.querySelector(this.SHOW_BTN);
        submitBtn.addEventListener("click", (e) => this.onSubmit(e));

        const viewSelect = document.querySelector(this.VIEW_SELECT);
        viewSelect.innerHTML = options;
        viewSelect.value = this.session.viewID || (Object.keys(this.views).length ? Object.keys(this.views)[0] : "");

        document.querySelector(this.WT_ID_TEXT).value = this.session.personName;

        if (document.querySelector(this.WT_ID_TEXT).value && viewSelect.value) submitBtn.click();
    }

    // When the "Go" button is clicked, grab the provided WikiTree ID and the selected View.
    // Currently we call getPerson() at the API to see that the provided ID is valid, and then launch the view.
    // Possibly this should be changed/removed. Different views require different incoming data, and some are just using the ID
    // and then immediately recalling getPerson() on that ID, which is a waste.
    onSubmit(e) {
        e.preventDefault();

        const wtID = document.querySelector(this.WT_ID_TEXT).value;
        const viewID = document.querySelector(this.VIEW_SELECT).value;

        const view = this.views[viewID];
        view.id = viewID;

        const viewLoader = document.querySelector(this.VIEW_LOADER);

        // This shouldn't happen, but perhaps we should display an error so new View builders can see what happened.
        if (view === undefined) return;

        viewLoader.classList.remove("hidden");

        const basicFields = ["Id", "Name", "FirstName", "LastName", "Derived.BirthName", "Derived.BirthNamePrivate"];

        try {
            WikiTreeAPI.postToAPI({
                action: "getPerson",
                key: wtID,
                fields: basicFields.join(),
            }).then((data) => this.onPersonDataReceived(view, data));
        } finally {
            viewLoader.classList.add("hidden");
        }
    }

    // After the initial getPerson from the onSubmit() launch returns, this method is called.
    onPersonDataReceived(view, data) {
        const wtID = document.querySelector(this.WT_ID_TEXT).value;
        const parentContainer = document.querySelector(this.NAME_PLACEHOLDER).closest("div");

        // If we have a person, go forward with launching the view, sending it the div ID to use for the display and the ID of the starting profile.
        // If we have no person, we show an error div.
        if (data[0]["person"]) {
            this.initView(view, data[0]["person"]);

            this.session.personID = data[0]["person"]["Id"];
            this.session.personName = data[0]["person"]["Name"];
            this.session.viewID = view.id;
            this.session.saveCookies();

            this.clearStatus();

            view.init(this.VIEW_CONTAINER, data[0]["person"]["Id"]);
            parentContainer.classList.remove("hidden");
        } else {
            parentContainer.classList.add("hidden");
            if (wtID) {
                this.showError(`Person not found for WikiTree ID ${wtID}.`);
            } else {
                this.showError("Please enter a WikiTree ID.");
            }
        }
    }

    initView(view, person) {
        const wtLink = document.querySelector(this.WT_ID_LINK);
        const viewTitle = document.querySelector(this.VIEW_TITLE);
        const viewDescription = document.querySelector(this.VIEW_DESCRIPTION);
        const name = document.querySelector(this.NAME_PLACEHOLDER);

        wtLink.href = `https://www.WikiTree.com/wiki/${person.Name}`;
        wtLink.innerHTML = person.Name;

        viewTitle.innerHTML = view.title;
        viewDescription.innerHTML = view.description;
        name.innerHTML = person.BirthName ? person.BirthName : person.BirthNamePrivate;

        document.querySelector(this.VIEW_CONTAINER).innerHTML = "";

        const wtID = document.querySelector(this.WT_ID_TEXT).value;
        const viewSelect = document.querySelector(this.VIEW_SELECT).value;
        let url = window.location.href.split("?")[0].split("#")[0];
        url = `${url}#name=${wtID}&view=${viewSelect}`;
        history.replaceState("", "", url);
    }

    clearStatus() {
        const wtStatus = document.querySelector(this.WT_STATUS);
        wtStatus.classList.add("hidden");
        wtStatus.classList.remove("red");
        wtStatus.classList.remove("green");
    }
    showError(msg) {
        const wtStatus = document.querySelector(this.WT_STATUS);
        this.clearStatus();
        wtStatus.innerHTML = msg;
        wtStatus.classList.add("red");
        wtStatus.classList.remove("hidden");
    }
    showWarning(msg) {
        const wtStatus = document.querySelector(this.WT_STATUS);
        this.clearStatus();
        wtStatus.innerHTML = msg;
        wtStatus.classList.remove("hidden");
    }
    showNotice(msg) {
        const wtStatus = document.querySelector(this.WT_STATUS);
        this.clearStatus();
        wtStatus.innerHTML = msg;
        wtStatus.classList.add("green");
        wtStatus.classList.remove("hidden");
    }
};

// This just stores the WikiTree ID (i.e. "name") and ID of the viewing user, if they are logged into the API.
window.WTUser = class WTUser {
    constructor(name = null, id = null) {
        this.name = name;
        this.id = id;
    }

    isLoggedIn() {
        return this.name && this.id;
    }
};

// This mediates the login to the WikiTree API.
// See: https://github.com/wikitree/wikitree-api/blob/main/authentication.md
window.LoginManager = class LoginManager {
    C_WT_USERNAME = "WikiTreeAPI_userName";
    C_WT_USER_ID = "WikiTreeAPI_userId";

    constructor(wtAPI, events = {}) {
        this.wtAPI = wtAPI;
        this.events = events;

        this.user = new WTUser();

        this.loadCookies();
    }

    loadCookies() {
        this.user.name = this.wtAPI.cookie(this.C_WT_USERNAME) || null;
        this.user.id = this.wtAPI.cookie(this.C_WT_USER_ID) || null;
    }

    saveCookies() {
        this.wtAPI.cookie(this.C_WT_USERNAME, this.user.name, { path: "/" });
        this.wtAPI.cookie(this.C_WT_USER_ID, this.user.id, { path: "/" });
    }

    login() {
        const searchParams = new URLSearchParams(location.search);
        const authcode = searchParams.get("authcode") ? searchParams.get("authcode") : null;

        if (this.user.isLoggedIn()) {
            this.events?.onLoggedIn(this.user);
        } else if (authcode) {
            // user is not logged in yet, but we've received authcode
            this.wtAPI.postToAPI({ action: "clientLogin", authcode: authcode }).then((data) => this.onAuth(data));
        } else {
            this.events?.onUnlogged();
        }
    }

    onAuth(data) {
        if (data.clientLogin.result === "Success") {
            this.user.name = data.clientLogin.username;
            this.user.id = data.clientLogin.userid;

            // changes browser url without additional redirect
            history.replaceState("", "", location.href.split("?")[0]);
            this.saveCookies();
            this.login();
        } else {
            this.events?.onAuthFail(data);
        }
    }
};
window.SessionManager = class SessionManager {
    C_PERSON_ID = "viewTreePersonId";
    C_PERSON_NAME = "viewTreePersonName";
    C_VIEW_ID = "viewTreeId";

    constructor(wtAPI, loginManager, events) {
        this.wtAPI = wtAPI;
        this.lm = loginManager;
        this.events = events || {};

        this.viewID = null;
        this.personID = null;
        this.personName = null;

        this.loadCookies();

        const orig_onLoggedIn_cb = this.lm.events?.onLoggedIn;
        this.lm.events["onLoggedIn"] = (user) => {
            this.personID ||= user.id;
            this.personName ||= user.name;

            orig_onLoggedIn_cb(user);
        };

        this.lm.login();
    }

    getHashParams(hash) {
        return new URLSearchParams(hash.slice(1));
    }

    loadUrlHash(viewIDs, urlHash) {
        const fields = this.getHashParams(urlHash);
        this.personName = fields.get("name") || this.personName;

        const viewID = fields.get("view");

        if (viewID && viewIDs.includes(viewID)) {
            this.viewID = fields.get("view");
        }
    }

    loadCookies() {
        this.viewID = this.wtAPI.cookie(this.C_VIEW_ID) || null;
        this.personID = this.wtAPI.cookie(this.C_PERSON_ID) || null;
        this.personName = this.wtAPI.cookie(this.C_PERSON_NAME) || null;
    }

    // For the version of the tree hosted on WikiTree.com itself, we want to be able to set the starting parameters by setting the cookies we use to store them.
    // Since the tree can be hosted at other paths (e.g. /treewidget/...), we need to store the cookies at the root "/" path so they are shared.
    saveCookies() {
        this.wtAPI.cookie(this.C_VIEW_ID, this.viewID, { path: "/" });
        this.wtAPI.cookie(this.C_PERSON_ID, this.personID, { path: "/" });
        this.wtAPI.cookie(this.C_PERSON_NAME, this.personName, { path: "/" });
    }
};
<commit_msg>fix: stays at selected profile after login<commit_after>/*
 * tree.js
 *
 * This file contains following functionalities
 *
 * Login manager
 *  - handles login process and cookies saving and loading to preserve logged in usr on the local machine
 *  - keeps info about logged in user with
 *
 * WTUser object
 * - properties: id:int, name: str
 * - methods: isLoggedIn() -> bool
 *
 * Session Manager
 *  - handles app status (lastly logged user Name and ID, lastly used view ID) and preserves it using cookies
 *  - takes control over Login manager and calls login method during app start
 *
 * View
 *  - contains basic methods and at the same time serves as a template for class based views
 *
 * View Registry
 *  - serves as main object that orchestrates everything
 *  - usage is demonstrated in index.html
 *
 * ---------------------------------------------------------------------------------------------------------------------
 *
 * How to add create new view and register it:
 *
 *  1. [class based] Create ancestor of View class and override method
 *    - meta: use you own title, description and docs
 *    - init: use your own implementation of view
 *
 *  or
 *
 *  1. [prototype based] Create similar structure as in class based approach and add
 *     following code into the constructor:
 *
 *     Object.assign(this, this?.meta()); // this will spread object into object fields for easier access
 *
 *     alternativelly, you can create those fields directly in constructor, e.g.: this.title = "Template view"
 *
 *  2. register view in ViewRegistry
 *
 *     a) link script file in header section of index.html, e.g.:
 *
 *        <script src="views/new_view/NewView.js"></script>
 *
 *     b) create new entry ("new-view-id": <NewViewObject>) in first parameter of ViewRegistry constructor (also in index.html), e.g.:
 *
 *        "new-view-id": new NewViewObject(),
 *
 *  3. Enjoy your newly registered view ;-)
 *
 */

window.View = class View {
    constructor() {
        this.id = null;
        Object.assign(this, this?.meta()); // this will spread object into object fields for easier access
    }

    meta() {
        return {
            title: "Template view",
            description: "Showcase of the views and their registration.",
            docs: "https://example.com",
        };
    }

    init(container_selector, person_id) {
        document.querySelector(container_selector).innerHTML = `Template View for person with ID: ${person_id}`;
    }
};

/*
 * The ViewRegistry holds the configuration for our collection of different views, builds the <select> field to change between them,
 * and launches the selected view when the "Go" button is clicked.
 */
window.ViewRegistry = class ViewRegistry {
    // These are all divs in index.html holding the various content sections.
    VIEW_SELECT = "#view-select";
    WT_ID_TEXT = "#wt-id-text";
    SHOW_BTN = "#show-btn";
    VIEW_CONTAINER = "#view-container";
    VIEW_TITLE = "#view-title";
    VIEW_DESCRIPTION = " #view-description";
    NAME_PLACEHOLDER = "#name-placeholder";
    WT_ID_LINK = " #wt-id-link";
    VIEW_LOADER = "#view-loader";
    WT_STATUS = "#wt-status";

    // index.html starts with a script that creates a new ViewRegistry, and then immediately calls .render() to update the selection form.
    constructor(views, session_manager) {
        this.views = views;
        this.session = session_manager;

        // This auto-launches the previously selected view (if there was one) when the page reloads.
        const orig_onLoggedIn_cb = this.session.lm.events?.onLoggedIn;
        this.session.lm.events["onLoggedIn"] = (user) => {
            if (!this.session.getHashParams(location.hash).get("name")) {
                document.querySelector(this.WT_ID_TEXT).value = user.name;
            }

            orig_onLoggedIn_cb(user);
            document.querySelector(this.SHOW_BTN).click();
        };

        if (location.hash) {
            this.session.loadUrlHash(Object.keys(views), location.hash);
        }

        addEventListener("hashchange", (e) => this.onHashChange(e));
    }

    onHashChange(e) {
        this.session.loadUrlHash(Object.keys(this.views), e.target.location.hash);

        document.querySelector(this.WT_ID_TEXT).value = this.session.personName;
        document.querySelector(this.VIEW_SELECT).value = this.session.viewID;
        document.querySelector(this.SHOW_BTN).click();
    }

    // Build the <select> option list from the individual views in the registry.
    // Add an event listener to the "go" button to call onSubmit() when clicked.
    // Fill in some data from the logged-in user.
    render() {
        let views = this.views;
        const options = Object.keys(this.views)
            .sort(function (a, b) {
                // We want the base/core option to always be at the top of the drop-down
                if (a == "wt-dynamic-tree") {
                    return -1;
                }
                if (b == "wt-dynamic-tree") {
                    return 1;
                }

                // Sort the rest alphabetically by title
                return views[a].title.localeCompare(views[b].title);
            })
            .map((id) => `<option value="${id}">${this.views[id].title}</option>`)
            .join("");

        const submitBtn = document.querySelector(this.SHOW_BTN);
        submitBtn.addEventListener("click", (e) => this.onSubmit(e));

        const viewSelect = document.querySelector(this.VIEW_SELECT);
        viewSelect.innerHTML = options;
        viewSelect.value = this.session.viewID || (Object.keys(this.views).length ? Object.keys(this.views)[0] : "");

        document.querySelector(this.WT_ID_TEXT).value = this.session.personName;

        if (document.querySelector(this.WT_ID_TEXT).value && viewSelect.value) submitBtn.click();
    }

    // When the "Go" button is clicked, grab the provided WikiTree ID and the selected View.
    // Currently we call getPerson() at the API to see that the provided ID is valid, and then launch the view.
    // Possibly this should be changed/removed. Different views require different incoming data, and some are just using the ID
    // and then immediately recalling getPerson() on that ID, which is a waste.
    onSubmit(e) {
        e.preventDefault();

        const wtID = document.querySelector(this.WT_ID_TEXT).value;
        const viewID = document.querySelector(this.VIEW_SELECT).value;

        const view = this.views[viewID];
        view.id = viewID;

        const viewLoader = document.querySelector(this.VIEW_LOADER);

        // This shouldn't happen, but perhaps we should display an error so new View builders can see what happened.
        if (view === undefined) return;

        viewLoader.classList.remove("hidden");

        const basicFields = ["Id", "Name", "FirstName", "LastName", "Derived.BirthName", "Derived.BirthNamePrivate"];

        try {
            WikiTreeAPI.postToAPI({
                action: "getPerson",
                key: wtID,
                fields: basicFields.join(),
            }).then((data) => this.onPersonDataReceived(view, data));
        } finally {
            viewLoader.classList.add("hidden");
        }
    }

    // After the initial getPerson from the onSubmit() launch returns, this method is called.
    onPersonDataReceived(view, data) {
        const wtID = document.querySelector(this.WT_ID_TEXT).value;
        const parentContainer = document.querySelector(this.NAME_PLACEHOLDER).closest("div");

        // If we have a person, go forward with launching the view, sending it the div ID to use for the display and the ID of the starting profile.
        // If we have no person, we show an error div.
        if (data[0]["person"]) {
            this.initView(view, data[0]["person"]);

            this.session.personID = data[0]["person"]["Id"];
            this.session.personName = data[0]["person"]["Name"];
            this.session.viewID = view.id;
            this.session.saveCookies();

            this.clearStatus();

            view.init(this.VIEW_CONTAINER, data[0]["person"]["Id"]);
            parentContainer.classList.remove("hidden");
        } else {
            parentContainer.classList.add("hidden");
            if (wtID) {
                this.showError(`Person not found for WikiTree ID ${wtID}.`);
            } else {
                this.showError("Please enter a WikiTree ID.");
            }
        }
    }

    initView(view, person) {
        const wtLink = document.querySelector(this.WT_ID_LINK);
        const viewTitle = document.querySelector(this.VIEW_TITLE);
        const viewDescription = document.querySelector(this.VIEW_DESCRIPTION);
        const name = document.querySelector(this.NAME_PLACEHOLDER);

        wtLink.href = `https://www.WikiTree.com/wiki/${person.Name}`;
        wtLink.innerHTML = person.Name;

        viewTitle.innerHTML = view.title;
        viewDescription.innerHTML = view.description;
        name.innerHTML = person.BirthName ? person.BirthName : person.BirthNamePrivate;

        document.querySelector(this.VIEW_CONTAINER).innerHTML = "";

        const wtID = document.querySelector(this.WT_ID_TEXT).value;
        const viewSelect = document.querySelector(this.VIEW_SELECT).value;
        let url = window.location.href.split("?")[0].split("#")[0];
        url = `${url}#name=${wtID}&view=${viewSelect}`;
        history.replaceState("", "", url);
    }

    clearStatus() {
        const wtStatus = document.querySelector(this.WT_STATUS);
        wtStatus.classList.add("hidden");
        wtStatus.classList.remove("red");
        wtStatus.classList.remove("green");
    }
    showError(msg) {
        const wtStatus = document.querySelector(this.WT_STATUS);
        this.clearStatus();
        wtStatus.innerHTML = msg;
        wtStatus.classList.add("red");
        wtStatus.classList.remove("hidden");
    }
    showWarning(msg) {
        const wtStatus = document.querySelector(this.WT_STATUS);
        this.clearStatus();
        wtStatus.innerHTML = msg;
        wtStatus.classList.remove("hidden");
    }
    showNotice(msg) {
        const wtStatus = document.querySelector(this.WT_STATUS);
        this.clearStatus();
        wtStatus.innerHTML = msg;
        wtStatus.classList.add("green");
        wtStatus.classList.remove("hidden");
    }
};

// This just stores the WikiTree ID (i.e. "name") and ID of the viewing user, if they are logged into the API.
window.WTUser = class WTUser {
    constructor(name = null, id = null) {
        this.name = name;
        this.id = id;
    }

    isLoggedIn() {
        return this.name && this.id;
    }
};

// This mediates the login to the WikiTree API.
// See: https://github.com/wikitree/wikitree-api/blob/main/authentication.md
window.LoginManager = class LoginManager {
    C_WT_USERNAME = "WikiTreeAPI_userName";
    C_WT_USER_ID = "WikiTreeAPI_userId";

    constructor(wtAPI, events = {}) {
        this.wtAPI = wtAPI;
        this.events = events;

        this.user = new WTUser();

        this.loadCookies();
    }

    loadCookies() {
        this.user.name = this.wtAPI.cookie(this.C_WT_USERNAME) || null;
        this.user.id = this.wtAPI.cookie(this.C_WT_USER_ID) || null;
    }

    saveCookies() {
        this.wtAPI.cookie(this.C_WT_USERNAME, this.user.name, { path: "/" });
        this.wtAPI.cookie(this.C_WT_USER_ID, this.user.id, { path: "/" });
    }

    login() {
        const searchParams = new URLSearchParams(location.search);
        const authcode = searchParams.get("authcode") ? searchParams.get("authcode") : null;

        if (this.user.isLoggedIn()) {
            this.events?.onLoggedIn(this.user);
        } else if (authcode) {
            // user is not logged in yet, but we've received authcode
            this.wtAPI.postToAPI({ action: "clientLogin", authcode: authcode }).then((data) => this.onAuth(data));
        } else {
            this.events?.onUnlogged();
        }
    }

    onAuth(data) {
        if (data.clientLogin.result === "Success") {
            this.user.name = data.clientLogin.username;
            this.user.id = data.clientLogin.userid;

            this.saveCookies();
            this.login();
        } else {
            this.events?.onAuthFail(data);
        }
    }
};
window.SessionManager = class SessionManager {
    C_PERSON_ID = "viewTreePersonId";
    C_PERSON_NAME = "viewTreePersonName";
    C_VIEW_ID = "viewTreeId";

    constructor(wtAPI, loginManager, events) {
        this.wtAPI = wtAPI;
        this.lm = loginManager;
        this.events = events || {};

        this.viewID = null;
        this.personID = null;
        this.personName = null;

        this.loadCookies();

        const orig_onLoggedIn_cb = this.lm.events?.onLoggedIn;
        this.lm.events["onLoggedIn"] = (user) => {
            this.personID ||= user.id;
            this.personName ||= user.name;

            orig_onLoggedIn_cb(user);
        };

        this.lm.login();
    }

    getHashParams(hash) {
        return new URLSearchParams(hash.slice(1));
    }

    loadUrlHash(viewIDs, urlHash) {
        const fields = this.getHashParams(urlHash);
        this.personName = fields.get("name") || this.personName;

        const viewID = fields.get("view");

        if (viewID && viewIDs.includes(viewID)) {
            this.viewID = fields.get("view");
        }
    }

    loadCookies() {
        this.viewID = this.wtAPI.cookie(this.C_VIEW_ID) || null;
        this.personID = this.wtAPI.cookie(this.C_PERSON_ID) || null;
        this.personName = this.wtAPI.cookie(this.C_PERSON_NAME) || null;
    }

    // For the version of the tree hosted on WikiTree.com itself, we want to be able to set the starting parameters by setting the cookies we use to store them.
    // Since the tree can be hosted at other paths (e.g. /treewidget/...), we need to store the cookies at the root "/" path so they are shared.
    saveCookies() {
        this.wtAPI.cookie(this.C_VIEW_ID, this.viewID, { path: "/" });
        this.wtAPI.cookie(this.C_PERSON_ID, this.personID, { path: "/" });
        this.wtAPI.cookie(this.C_PERSON_NAME, this.personName, { path: "/" });
    }
};
<|endoftext|>